<?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: Mihai-Cristian Bâltac</title>
    <description>The latest articles on DEV Community by Mihai-Cristian Bâltac (@baltacmihai).</description>
    <link>https://dev.to/baltacmihai</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%2F3760479%2F0d652fe8-84ad-4c34-bf7c-da9b6c8be0d8.png</url>
      <title>DEV Community: Mihai-Cristian Bâltac</title>
      <link>https://dev.to/baltacmihai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/baltacmihai"/>
    <language>en</language>
    <item>
      <title>We Built a Chatbot Without an LLM: Here’s How It Works</title>
      <dc:creator>Mihai-Cristian Bâltac</dc:creator>
      <pubDate>Fri, 04 Sep 2026 07:30:15 +0000</pubDate>
      <link>https://dev.to/baltacmihai/we-built-a-chatbot-without-an-llm-heres-how-it-works-39o6</link>
      <guid>https://dev.to/baltacmihai/we-built-a-chatbot-without-an-llm-heres-how-it-works-39o6</guid>
      <description>&lt;p&gt;When you hear "chatbot" in 2026, the obvious architecture is something like:&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%2F4vahumufbdmebs5jlx9n.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%2F4vahumufbdmebs5jlx9n.png" alt=" " width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For one of our projects, we deliberately didn't do that.&lt;/p&gt;

&lt;p&gt;The chatbot needed to answer questions about things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;prices;&lt;/li&gt;
&lt;li&gt;opening hours;&lt;/li&gt;
&lt;li&gt;appointments;&lt;/li&gt;
&lt;li&gt;service availability;&lt;/li&gt;
&lt;li&gt;business policies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For those kinds of questions, we cared more about predictability than creativity.&lt;/p&gt;

&lt;p&gt;If the business says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We only work by appointment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;we don't want a model to turn that into:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Appointments are recommended, but you may be able to come without one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It sounds helpful.&lt;/p&gt;

&lt;p&gt;It is also wrong.&lt;/p&gt;

&lt;p&gt;So we built a deterministic chatbot with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Astro;&lt;/li&gt;
&lt;li&gt;TypeScript;&lt;/li&gt;
&lt;li&gt;Sanity;&lt;/li&gt;
&lt;li&gt;Fuse.js.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No LLM generates the customer-facing answers.&lt;/p&gt;

&lt;p&gt;Here's how it works.&lt;/p&gt;




&lt;h2&gt;
  
  
  The basic idea
&lt;/h2&gt;

&lt;p&gt;Instead of asking a model:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What should I answer?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;we ask our system:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which known intent does this question most likely belong to?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The business controls the actual response.&lt;/p&gt;

&lt;p&gt;Conceptually:&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%2Fsv4wy6eoxmvopmy5fkct.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%2Fsv4wy6eoxmvopmy5fkct.png" alt=" " width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The important part isn't actually Fuse.js.&lt;/p&gt;

&lt;p&gt;It's everything around it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sanity is the knowledge source
&lt;/h2&gt;

&lt;p&gt;We didn't want prices, answers, keywords, or conversation options buried inside the application code.&lt;/p&gt;

&lt;p&gt;An intent can look approximately like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ChatIntent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;

  &lt;span class="nx"&gt;phrases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;negativeKeywords&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;

  &lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;

  &lt;span class="nx"&gt;contextTags&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;requiredContextTags&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="nx"&gt;buttons&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;ChatButton&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Consultation price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phrases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"How much does a consultation cost?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"What is the price of a consultation?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"What do you charge for a consultation?"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"charge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"consultation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"consult"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A consultation costs..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation turned out to be useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The matcher decides what the user means.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The business decides what the answer is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the business changes a price or opening hour, it can be updated from Sanity without changing the matching algorithm.&lt;/p&gt;




&lt;h2&gt;
  
  
  First, normalize everything
&lt;/h2&gt;

&lt;p&gt;Real users don't type like your test data.&lt;/p&gt;

&lt;p&gt;They write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;how much consult
consultation price???
HOW MUCH
how mutch is consultation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, in Romanian:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat costa consultatia
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cât costă consultația?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So before matching anything, we normalize the input.&lt;/p&gt;

&lt;p&gt;A simplified version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NFD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\p&lt;/span&gt;&lt;span class="sr"&gt;{Diacritic}/gu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^\p&lt;/span&gt;&lt;span class="sr"&gt;{L}&lt;/span&gt;&lt;span class="se"&gt;\p&lt;/span&gt;&lt;span class="sr"&gt;{N}&lt;/span&gt;&lt;span class="se"&gt;\s]&lt;/span&gt;&lt;span class="sr"&gt;/gu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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;Normalization gets rid of a surprising amount of unnecessary complexity.&lt;/p&gt;

&lt;p&gt;But it isn't enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exact matches should still win
&lt;/h2&gt;

&lt;p&gt;Before doing anything clever, check the obvious cases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;exactPhraseMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;phrases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;phrases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;phrase&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;normalizeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;message&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;If the user asks exactly something we already know, there is little reason to rely on fuzzy matching.&lt;/p&gt;

&lt;p&gt;We can also look for known phrases inside longer messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;containedPhraseMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;phrases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;phrases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phrase&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;normalizeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;phrase&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;But things become harder when someone writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hi, I have a dog and I'd like to know roughly how much it would cost to bring him in for a consultation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's where multiple signals become useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keywords help, but they aren't the answer
&lt;/h2&gt;

&lt;p&gt;An intent about consultation pricing might contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;charge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consult&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consultation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can calculate keyword coverage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;keywordCoverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;normalizeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But imagine the user only writes:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;We might have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;consultation price
vaccine price
subscription price
analysis price
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Technically, they all match.&lt;/p&gt;

&lt;p&gt;So keywords become another signal rather than the decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then comes fuzzy matching
&lt;/h2&gt;

&lt;p&gt;We use Fuse.js to catch approximate wording and typos.&lt;/p&gt;

&lt;p&gt;Something roughly like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Fuse&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fuse.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fuse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Fuse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchablePhrases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;includeScore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&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;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fuse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizedMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helps with variations such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;consultation
consutation
consultaton
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this is where one of the more important lessons from the project appeared:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The best fuzzy result isn't necessarily a safe answer.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fuse will try to find the nearest thing.&lt;/p&gt;

&lt;p&gt;Our chatbot needs to decide whether that nearest thing is actually good enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  So we combine signals
&lt;/h2&gt;

&lt;p&gt;Conceptually, each candidate gets something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MatchSignals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;exactPhrase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;containedPhrase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;keywordCoverage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;fuzzySimilarity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;contextBoost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;priorityBoost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;negativePenalty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And those signals can contribute to a score:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MatchSignals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exactPhrase&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;containedPhrase&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keywordCoverage&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fuzzySimilarity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contextBoost&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priorityBoost&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;
    &lt;span class="nx"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;negativePenalty&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;Those weights are illustrative.&lt;/p&gt;

&lt;p&gt;The real point is the architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exact phrase
      +
Keywords
      +
Fuzzy similarity
      +
Context
      +
Priority
      -
Negative signals
      ↓
Confidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No single signal gets complete control.&lt;/p&gt;




&lt;h2&gt;
  
  
  Negative keywords were surprisingly useful
&lt;/h2&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;consultation price
cancel consultation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both contain &lt;code&gt;consultation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the pricing intent we might have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;price&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;charge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but also:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;negativeKeywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancellation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reschedule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If someone writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do I cancel my consultation?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;the word &lt;code&gt;consultation&lt;/code&gt; helps both candidates, but &lt;code&gt;cancel&lt;/code&gt; actively hurts the pricing candidate.&lt;/p&gt;

&lt;p&gt;Sometimes &lt;strong&gt;knowing what an intent isn't&lt;/strong&gt; is almost as useful as knowing what it is.&lt;/p&gt;




&lt;h2&gt;
  
  
  The most important check: ambiguity
&lt;/h2&gt;

&lt;p&gt;Suppose our matcher returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consultation-price&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.81&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscription-price&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.79&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;Technically, &lt;code&gt;consultation-price&lt;/code&gt; won.&lt;/p&gt;

&lt;p&gt;But did it really?&lt;/p&gt;

&lt;p&gt;The difference is:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;We don't want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, we can use both an answer threshold and an ambiguity margin.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ANSWER_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AMBIGUITY_MARGIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;matches&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ANSWER_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;second&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
  &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;AMBIGUITY_MARGIN&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;clarification&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, the numbers are only examples.&lt;/p&gt;

&lt;p&gt;The idea is much more important:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A candidate isn't trustworthy merely because it came first.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  We have three outcomes
&lt;/h2&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;we use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;MatchResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;answer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatIntent&lt;/span&gt;
      &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clarify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="na"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatIntent&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fallback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. Answer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User:
How much does a consultation cost?

Bot:
A consultation costs...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Clarify
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User:
How much does it cost?

Bot:
Which service would you like the price for?

[Consultation]
[Tests]
[Subscription]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Fallback
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User:
I have a complicated situation...

Bot:
I don't have enough information to answer that correctly.

Would you like me to send your question to the team?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this project, &lt;strong&gt;refusing to answer is a feature&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about follow-up questions?
&lt;/h2&gt;

&lt;p&gt;Then we ran into conversations like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User:
How much does the consultation cost?

Bot:
...

User:
And what does it include?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Analyzed independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;and what does it include
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is almost useless.&lt;/p&gt;

&lt;p&gt;So we keep lightweight conversation context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ConversationContext&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;previousIntent&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;activeTopic&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;contextTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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 the first question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;previousIntent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consultation-price&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;activeTopic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consultation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;contextTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consultation&lt;/span&gt;&lt;span class="dl"&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;Another intent can require:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;requiredContextTags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consultation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and receive a small scoring boost.&lt;/p&gt;

&lt;p&gt;We don't need an LLM-sized memory system for every type of conversational context.&lt;/p&gt;

&lt;p&gt;Sometimes remembering &lt;strong&gt;what we're currently talking about&lt;/strong&gt; is enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Astro exposes the matcher
&lt;/h2&gt;

&lt;p&gt;The UI doesn't contain the matching logic.&lt;/p&gt;

&lt;p&gt;It sends the message to an Astro API endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/chatbot/message
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"how much does a consultation cost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;astro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;matchMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/chatbot/matcher&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;matchMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="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 frontend receives a predictable result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A consultation costs..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buttons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Book an appointment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also means we can replace or redesign the chat UI without rewriting the matcher.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sanity shouldn't sit in front of every message
&lt;/h2&gt;

&lt;p&gt;The intents don't change every few seconds.&lt;/p&gt;

&lt;p&gt;So querying Sanity for every user message would add unnecessary work.&lt;/p&gt;

&lt;p&gt;Instead, the knowledge base can be cached:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cachedKnowledge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KnowledgeBase&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getKnowledge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;cachedKnowledge&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;expiresAt&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cachedKnowledge&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;intents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchIntentsFromSanity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nx"&gt;cachedKnowledge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildKnowledgeBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;expiresAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CACHE_TTL&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cachedKnowledge&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sanity remains the source of truth.&lt;/p&gt;

&lt;p&gt;It doesn't necessarily need to be part of the critical path for every message.&lt;/p&gt;




&lt;h2&gt;
  
  
  The unexpected part: unanswered questions are useful
&lt;/h2&gt;

&lt;p&gt;Originally, the goal was straightforward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Reduce repetitive customer-support questions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But then we started thinking about the fallback data.&lt;/p&gt;

&lt;p&gt;Imagine seeing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;37 × "do you provide emergency services?"
21 × "can I pay monthly?"
18 × "are you open on Saturdays?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those aren't only chatbot failures.&lt;/p&gt;

&lt;p&gt;They're customer signals.&lt;/p&gt;

&lt;p&gt;They can indicate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a missing chatbot intent;&lt;/li&gt;
&lt;li&gt;unclear website content;&lt;/li&gt;
&lt;li&gt;terminology customers use that the business doesn't;&lt;/li&gt;
&lt;li&gt;a potential article;&lt;/li&gt;
&lt;li&gt;a UX problem;&lt;/li&gt;
&lt;li&gt;a possible new service or package.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This changed how I think about the system.&lt;/p&gt;

&lt;p&gt;The chatbot isn't only an answering machine.&lt;/p&gt;

&lt;p&gt;It can also become a &lt;strong&gt;customer research interface&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ironically, this is where AI becomes interesting
&lt;/h2&gt;

&lt;p&gt;We deliberately avoided generative AI for official answers.&lt;/p&gt;

&lt;p&gt;But I think AI could be extremely useful one step later.&lt;/p&gt;

&lt;p&gt;Imagine collecting 500 unanswered questions and asking a model to cluster them.&lt;/p&gt;

&lt;p&gt;It might identify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cluster: Emergency availability

- do you handle emergencies?
- can I come in urgently?
- do you offer emergency consultations?
- do you accept emergencies at night?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a human decides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should we create another intent?&lt;/li&gt;
&lt;li&gt;Is the website missing important information?&lt;/li&gt;
&lt;li&gt;Are customers using different terminology?&lt;/li&gt;
&lt;li&gt;Is there demand for something the business doesn't currently offer?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives us a separation I like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI → analysis

Deterministic system → official answers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not really "AI vs no AI."&lt;/p&gt;

&lt;p&gt;It's about putting each tool in the part of the system where its characteristics are useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  The main thing I learned
&lt;/h2&gt;

&lt;p&gt;The difficult part of this chatbot wasn't teaching it to answer questions.&lt;/p&gt;

&lt;p&gt;It was teaching it &lt;strong&gt;when not to answer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A fuzzy search system can almost always find something that looks similar.&lt;/p&gt;

&lt;p&gt;A trustworthy system needs another capability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I found something,
but I'm not confident enough to use it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For prices, schedules, policies, service conditions, and similar business information, that behavior can be more valuable than generating a natural-sounding response every time.&lt;/p&gt;

&lt;p&gt;And the questions it refuses to answer?&lt;/p&gt;

&lt;p&gt;Those may eventually become the most interesting data in the whole system.&lt;/p&gt;




&lt;p&gt;If you're interested in the longer implementation guide and the product reasoning behind the experiment, I've documented the project in more detail on the &lt;a href="https://digitalempr.ro/cercetare-si-dezvoltare" rel="noopener noreferrer"&gt;Digital Empr Research &amp;amp; Development site&lt;/a&gt;. &lt;em&gt;Unfortunately, the website is currently only available in Romanian, but we’re planning to translate it into English soon.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I designed and implemented the system described here. AI tools were used to assist with editing and structuring this article; the technical decisions and project experience are my own.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>automation</category>
      <category>agents</category>
    </item>
    <item>
      <title>Marketing Started Making Sense When I Stopped Seeing It as Content and Started Seeing It as Infrastructure</title>
      <dc:creator>Mihai-Cristian Bâltac</dc:creator>
      <pubDate>Tue, 21 Apr 2026 08:49:19 +0000</pubDate>
      <link>https://dev.to/baltacmihai/marketing-started-making-sense-when-i-stopped-seeing-it-as-content-and-started-seeing-it-as-jhj</link>
      <guid>https://dev.to/baltacmihai/marketing-started-making-sense-when-i-stopped-seeing-it-as-content-and-started-seeing-it-as-jhj</guid>
      <description>&lt;p&gt;For a long time, marketing felt vague to me.&lt;/p&gt;

&lt;p&gt;As a developer, I was used to things that were concrete: inputs, outputs, dependencies, systems.&lt;/p&gt;

&lt;p&gt;Marketing seemed to live in a different world. People talked about building a brand, posting consistently, creating awareness, increasing trust.&lt;/p&gt;

&lt;p&gt;None of that was wrong.&lt;/p&gt;

&lt;p&gt;But a lot of it felt too abstract to respect.&lt;/p&gt;

&lt;p&gt;What finally made marketing click for me was a simple shift:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I stopped seeing it as content, and started seeing it as infrastructure.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mistake I Was Making
&lt;/h2&gt;

&lt;p&gt;Like a lot of technical people, I was only seeing the visible layer.&lt;/p&gt;

&lt;p&gt;I saw:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;posts&lt;/li&gt;
&lt;li&gt;ads&lt;/li&gt;
&lt;li&gt;blog articles&lt;/li&gt;
&lt;li&gt;emails&lt;/li&gt;
&lt;li&gt;headlines&lt;/li&gt;
&lt;li&gt;hooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I was missing was everything underneath:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;where leads actually go&lt;/li&gt;
&lt;li&gt;what gets measured&lt;/li&gt;
&lt;li&gt;what breaks trust&lt;/li&gt;
&lt;li&gt;where people drop off&lt;/li&gt;
&lt;li&gt;how fast a team can react&lt;/li&gt;
&lt;li&gt;how much manual work sits behind “simple” marketing actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once I started paying attention to that layer, marketing stopped feeling fluffy.&lt;/p&gt;

&lt;p&gt;It started feeling familiar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Became Real
&lt;/h2&gt;

&lt;p&gt;The shift happened when I started looking at websites less like deliverables and more like parts of larger systems.&lt;/p&gt;

&lt;p&gt;A form is not just a form.&lt;/p&gt;

&lt;p&gt;It is also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lead capture&lt;/li&gt;
&lt;li&gt;routing&lt;/li&gt;
&lt;li&gt;filtering&lt;/li&gt;
&lt;li&gt;tracking&lt;/li&gt;
&lt;li&gt;follow-up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An events page is not just an events page.&lt;/p&gt;

&lt;p&gt;It can also be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a source of truth&lt;/li&gt;
&lt;li&gt;a publishing workflow&lt;/li&gt;
&lt;li&gt;a distribution problem&lt;/li&gt;
&lt;li&gt;an operations problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was the shift.&lt;/p&gt;

&lt;p&gt;I stopped asking only:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How should this page look?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And started asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What system is this page part of?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Example
&lt;/h2&gt;

&lt;p&gt;One of the clearest examples was a business updating event information in more than one place.&lt;/p&gt;

&lt;p&gt;The website had one version. Facebook had another. Sometimes one got updated and the other did not.&lt;/p&gt;

&lt;p&gt;At first, that looks like a content problem.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;It is a system problem.&lt;/p&gt;

&lt;p&gt;The real fix is not “write better.”&lt;br&gt;
The real fix is to make one place the source of truth and reduce duplication.&lt;/p&gt;

&lt;p&gt;That is when marketing started making more sense to me.&lt;/p&gt;

&lt;p&gt;Because a lot of “marketing work” is really about reducing friction, inconsistency, and delay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Developers Have an Advantage Here
&lt;/h2&gt;

&lt;p&gt;Developers are not automatically better marketers.&lt;/p&gt;

&lt;p&gt;But we do have one useful advantage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;we are trained to think in systems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That matters more than it seems.&lt;/p&gt;

&lt;p&gt;Because once you look past the surface, a lot of marketing becomes a question of structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what happens before attention&lt;/li&gt;
&lt;li&gt;what happens after someone clicks&lt;/li&gt;
&lt;li&gt;what gets tracked&lt;/li&gt;
&lt;li&gt;what gets repeated&lt;/li&gt;
&lt;li&gt;what creates friction&lt;/li&gt;
&lt;li&gt;what slows teams down&lt;/li&gt;
&lt;li&gt;what breaks consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That mindset transfers well.&lt;/p&gt;

&lt;p&gt;It helps you notice that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;traffic without routing is incomplete&lt;/li&gt;
&lt;li&gt;content without distribution logic gets wasted&lt;/li&gt;
&lt;li&gt;tracking without trust is misleading&lt;/li&gt;
&lt;li&gt;a CMS is not just an editor, but a control layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was the point where marketing became easier to respect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why “Infrastructure” Is the Right Word
&lt;/h2&gt;

&lt;p&gt;Content is visible.&lt;/p&gt;

&lt;p&gt;Infrastructure is what makes content:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repeatable&lt;/li&gt;
&lt;li&gt;measurable&lt;/li&gt;
&lt;li&gt;connected&lt;/li&gt;
&lt;li&gt;easier to maintain&lt;/li&gt;
&lt;li&gt;easier to scale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that is where a lot of business value actually comes from.&lt;/p&gt;

&lt;p&gt;Not one perfect campaign.&lt;/p&gt;

&lt;p&gt;A better system.&lt;/p&gt;

&lt;p&gt;A lot of marketing problems are really system problems in disguise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;weak handoff after a lead arrives&lt;/li&gt;
&lt;li&gt;disconnected tools&lt;/li&gt;
&lt;li&gt;duplicated updates&lt;/li&gt;
&lt;li&gt;poor measurement&lt;/li&gt;
&lt;li&gt;too much manual work&lt;/li&gt;
&lt;li&gt;friction in simple workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I no longer see marketing and implementation as separate worlds.&lt;/p&gt;

&lt;p&gt;In many cases, they are the same problem viewed from different angles.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Believe Now
&lt;/h2&gt;

&lt;p&gt;I used to think marketing was mostly about content.&lt;/p&gt;

&lt;p&gt;Now I think content is just one visible expression of a larger system.&lt;/p&gt;

&lt;p&gt;Marketing started making sense when I saw it as infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how attention is captured&lt;/li&gt;
&lt;li&gt;how trust is reinforced&lt;/li&gt;
&lt;li&gt;how leads are routed&lt;/li&gt;
&lt;li&gt;how actions are tracked&lt;/li&gt;
&lt;li&gt;how information stays consistent&lt;/li&gt;
&lt;li&gt;how teams move faster with less friction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That shift changed how I think about websites, CMS setups, automations, reporting, and even simple forms.&lt;/p&gt;

&lt;p&gt;Because a lot of the time, the most useful thing you can build is not another feature.&lt;/p&gt;

&lt;p&gt;It is a cleaner system.&lt;/p&gt;

&lt;p&gt;If you're curious what this looks like in practice, here are the&lt;a href="https://digitalempr.ro/servicii" rel="noopener noreferrer"&gt; kinds of problems I currently help solve&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Has marketing ever made more sense to you once you stopped seeing it as promotion and started seeing it as a system?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>marketing</category>
      <category>rpa</category>
      <category>automation</category>
    </item>
    <item>
      <title>I Thought Good Software Would Market Itself. I Was Wrong.</title>
      <dc:creator>Mihai-Cristian Bâltac</dc:creator>
      <pubDate>Tue, 14 Apr 2026 10:42:23 +0000</pubDate>
      <link>https://dev.to/baltacmihai/i-thought-good-software-would-market-itself-i-was-wrong-3eom</link>
      <guid>https://dev.to/baltacmihai/i-thought-good-software-would-market-itself-i-was-wrong-3eom</guid>
      <description>&lt;p&gt;For most of my career, I believed something a lot of technical people quietly believe:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you build something genuinely good, people will notice.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Clean code. Good UX. Fast performance. Real value.&lt;/p&gt;

&lt;p&gt;Surely that should be enough.&lt;/p&gt;

&lt;p&gt;It was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Belief That Broke
&lt;/h2&gt;

&lt;p&gt;My background was mostly in frontend, UI, and later full stack work. For years, I thought like a builder first: make the product better, make the system stronger, and the results will follow.&lt;/p&gt;

&lt;p&gt;That belief feels rational, especially when you spend most of your time around other technical people.&lt;/p&gt;

&lt;p&gt;But the moment you try to build something outside your job, you run into a different reality:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;good work does not automatically create attention.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Started Questioning It
&lt;/h2&gt;

&lt;p&gt;Then the market changed.&lt;/p&gt;

&lt;p&gt;AI got better. Layoffs became more common. More teams started doing more with fewer people. More non-technical people started building tools and workflows with AI.&lt;/p&gt;

&lt;p&gt;At the same time, I kept hearing the same kind of quiet anxiety from people in tech:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Every quarter we have another meeting where they announce the team will shrink again.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That pushed me into a question I had avoided for a long time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If being a good developer is no longer enough, what else do I need to become?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wrong Assumption
&lt;/h2&gt;

&lt;p&gt;When I started building things outside my job, I made a very common mistake.&lt;/p&gt;

&lt;p&gt;I thought quality would create demand.&lt;/p&gt;

&lt;p&gt;I thought if I made something useful, polished, and technically strong, people would come. Maybe through referrals, word of mouth, or “organic growth.”&lt;/p&gt;

&lt;p&gt;It sounds reasonable.&lt;/p&gt;

&lt;p&gt;It is also one of the biggest lies builders tell themselves.&lt;/p&gt;

&lt;p&gt;A good product does &lt;strong&gt;not&lt;/strong&gt; create traffic by itself.&lt;/p&gt;

&lt;p&gt;A good website does &lt;strong&gt;not&lt;/strong&gt; create attention by itself.&lt;/p&gt;

&lt;p&gt;A good app does &lt;strong&gt;not&lt;/strong&gt; create customers by itself.&lt;/p&gt;

&lt;p&gt;What good software does is this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;once people arrive, it helps convert, retain, and impress them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But first, people have to arrive.&lt;/p&gt;

&lt;p&gt;That is where marketing starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Frustrated Me About Marketing
&lt;/h2&gt;

&lt;p&gt;When I started taking marketing seriously, it felt frustratingly vague.&lt;/p&gt;

&lt;p&gt;As developers, we are used to practical steps:&lt;br&gt;
install this, configure this, test this, fix this.&lt;/p&gt;

&lt;p&gt;Marketing felt full of advice like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;post consistently&lt;/li&gt;
&lt;li&gt;know your audience&lt;/li&gt;
&lt;li&gt;provide value&lt;/li&gt;
&lt;li&gt;build your brand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is wrong.&lt;/p&gt;

&lt;p&gt;But when you are starting, it can feel too abstract to act on.&lt;/p&gt;

&lt;p&gt;So I approached marketing the same way I eventually learned programming:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;learn the concepts&lt;/li&gt;
&lt;li&gt;understand the mental models&lt;/li&gt;
&lt;li&gt;test things in the real world&lt;/li&gt;
&lt;li&gt;keep what works&lt;/li&gt;
&lt;li&gt;stop confusing theory with progress&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That changed the way I looked at it.&lt;/p&gt;

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

&lt;p&gt;The hardest lesson was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;building something good and getting attention for it are two different skills.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A lot of technical people overvalue creation and undervalue distribution. I did too.&lt;/p&gt;

&lt;p&gt;We assume the hard part is making the product.&lt;/p&gt;

&lt;p&gt;Sometimes it is.&lt;/p&gt;

&lt;p&gt;But often the harder part is making the right people aware that it exists, understand why it matters, and trust it enough to try it.&lt;/p&gt;

&lt;p&gt;I also had another wrong assumption:&lt;/p&gt;

&lt;p&gt;I thought that if I wanted a problem solved badly enough, a lot of other people probably wanted the same thing.&lt;/p&gt;

&lt;p&gt;Sometimes that is true.&lt;/p&gt;

&lt;p&gt;Often it is not.&lt;/p&gt;

&lt;p&gt;A lot of developer-built products are polished versions of our own frustrations. That is not automatically bad, but it is not the same thing as market demand.&lt;/p&gt;

&lt;p&gt;To understand what other people actually need, you have to do something many developers avoid:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;talk to people. A lot of them.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Developers Still Have an Advantage
&lt;/h2&gt;

&lt;p&gt;The interesting part is that once I stopped expecting software to market itself, I started seeing a different advantage.&lt;/p&gt;

&lt;p&gt;Developers are not automatically good at marketing.&lt;/p&gt;

&lt;p&gt;But good developers do have one big advantage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;we are trained to think in systems.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that matters more than I expected.&lt;/p&gt;

&lt;p&gt;Because once you move past the surface-level idea of marketing, a lot of it becomes a question of structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what happens before attention&lt;/li&gt;
&lt;li&gt;what happens after someone clicks&lt;/li&gt;
&lt;li&gt;where people drop off&lt;/li&gt;
&lt;li&gt;what gets measured&lt;/li&gt;
&lt;li&gt;what creates friction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was familiar territory.&lt;/p&gt;

&lt;p&gt;I stopped seeing marketing as “making posts” and started seeing it as part communication, part systems design, part customer understanding.&lt;/p&gt;

&lt;p&gt;That made it much easier to take seriously.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Believe Now
&lt;/h2&gt;

&lt;p&gt;I no longer believe that good software markets itself.&lt;/p&gt;

&lt;p&gt;I believe this instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;good software matters once attention already exists&lt;/li&gt;
&lt;li&gt;distribution is a real skill, not an afterthought&lt;/li&gt;
&lt;li&gt;understanding business makes technical work more valuable&lt;/li&gt;
&lt;li&gt;talking to customers beats guessing&lt;/li&gt;
&lt;li&gt;building is important, but it is not the whole game&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The market is getting noisier. More people can create. More tools reduce the gap. More companies buy outcomes, not technical elegance.&lt;/p&gt;

&lt;p&gt;So the question is no longer just:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you build it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is also:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can you understand who it is for, why it matters, and how it reaches the right people?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the shift I am trying to make now.&lt;/p&gt;

&lt;p&gt;And honestly, I think more developers should make it too.&lt;/p&gt;




&lt;p&gt;What changed your perspective more: building products, or trying to get people to care about them?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>marketing</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Has anyone managed to create a custom Gem in Gemini for image generation that actually works well?</title>
      <dc:creator>Mihai-Cristian Bâltac</dc:creator>
      <pubDate>Tue, 24 Mar 2026 18:41:06 +0000</pubDate>
      <link>https://dev.to/baltacmihai/has-anyone-managed-to-create-a-custom-gem-in-gemini-for-image-generation-that-actually-works-well-4280</link>
      <guid>https://dev.to/baltacmihai/has-anyone-managed-to-create-a-custom-gem-in-gemini-for-image-generation-that-actually-works-well-4280</guid>
      <description>&lt;p&gt;I’ve tried a lot of approaches: giving it JSON instructions, adding reference images, and describing the style in detail, but the results still don’t follow the intended look consistently.&lt;/p&gt;

&lt;p&gt;What I’m trying to achieve is simple:&lt;br&gt;
I want to upload one image and have Gemini generate a new image in the same visual style.&lt;/p&gt;

&lt;p&gt;At the moment, it doesn’t seem to handle that reliably.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>discuss</category>
      <category>gemini</category>
      <category>google</category>
    </item>
  </channel>
</rss>
