<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="https://www.mindfiretechnology.com/blog/rss/xslt"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>Mindfire Technology</title>
    <link>https://www.mindfiretechnology.com/blog/</link>
    <description>Welcome to our blog, where we share technical and business knowledge based on real life experiences.</description>
    <generator>Articulate, blogging built on Umbraco</generator>
    <item>
      <guid isPermaLink="false">2697</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/adventures-in-langchains-quick-start-tutorial-using-ollama/</link>
      <category>System.String[]</category>
      <title>Adventures in LangChain's "Quick Start Tutorial" (Using Ollama)</title>
      <description>&lt;p&gt;Suppose you want to learn &lt;a href="https://www.langchain.com/"&gt;LangChain&lt;/a&gt;, so naturally you go to their &lt;a href="https://docs.langchain.com/oss/python/langchain/quickstart"&gt;quick start tutorial page&lt;/a&gt;. And here is what you find as their first suggested tutorial:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from langchain.agents import create_agent

def get_weather(city: str) -&amp;gt; str:
    &amp;quot;&amp;quot;&amp;quot;Get weather for a given city.&amp;quot;&amp;quot;&amp;quot;
    return f&amp;quot;It's always sunny in {city}!&amp;quot;

agent = create_agent(
    model=&amp;quot;claude-sonnet-4-5-20250929&amp;quot;,
    tools=[get_weather],
    system_prompt=&amp;quot;You are a helpful assistant&amp;quot;,
)

# Run the agent
agent.invoke(
    {&amp;quot;messages&amp;quot;: [{&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: &amp;quot;what is the weather in sf&amp;quot;}]}
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Okay, but this requires you to run &lt;a href="https://claude.ai/"&gt;Claude&lt;/a&gt;. But doesn't Claude require an api key? Well, it doesn't say anything about that. Maybe there's a free Claude api tier that requires no key? &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Could not resolve authentication method. Expected either api_key or auth_token to be set.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Oops, nope. You need a Claude api key. I guess they were assuming I'd just know that and already have it set in my environment? Seems like a weird assumption for a quick start tutorial, but okay, I guess?&lt;/p&gt;
&lt;p&gt;No worries, I'll just go get a Claud api key. &lt;/p&gt;
&lt;p&gt;But wait, doesn't Claude require me to pay for tokens?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/claude.png" alt="Claude Free Plan!" /&gt;&lt;/p&gt;
&lt;p&gt;Oh, whew! Claude has a free tier for testing! Good! Okay, not too bad. Let's try that quick start tutorial again.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Your credit balance is too low to access the Anthropic API. Please go to Plans &amp;amp; Billing to upgrade or purchase credits.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Arg! Okay, apparently Claude is lying and there is no free tier! Even though I took that verbiage directly off the API key page for Claude, I guess they meant I only had access to the web interface? &lt;/p&gt;
&lt;p&gt;I tried asking the &lt;a href="https://chat.langchain.com/"&gt;LangChain AI&lt;/a&gt; why this didn't work and it recommended I try out the OpenAI free tier instead and even helpfully gave me a rewrite to the 'quick start tutorial' that would do this. &lt;/p&gt;
&lt;p&gt;Wait, doesn't OpenAI have no free tier? Didn't they sunset that like two seconds after they went live and ended up with so many users? But surely the LangChain AI knows what it's talking about, right? &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nope, the LangChain AI doesn't have a clue. &lt;/p&gt;
&lt;p&gt;Okay, no worries, I'll just rewrite this 'quick start tutorial' to use ollama. Here is what I try:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# pip pip install -U langchain langchain-ollama
# Requires Python 3.10+
# pip install -U langchain langchain-ollama
# Requires Ollama installed + &amp;quot;ollama pull llama3.2&amp;quot;
from langchain_ollama import ChatOllama  # Changed from OpenAI
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def get_weather(city: str) -&amp;gt; str:
    &amp;quot;&amp;quot;&amp;quot;Get weather for a given city.&amp;quot;&amp;quot;&amp;quot;
    return f&amp;quot;It's always sunny in {city}!&amp;quot;

# No secrets needed!
llm = ChatOllama(model=&amp;quot;llama3.2:1b&amp;quot;)  # Free local model
agent = create_agent(
    model=llm,
    tools=[get_weather],
    system_prompt=&amp;quot;You are a helpful assistant.&amp;quot;,
)

# Run agent
# Our message
message = {
    &amp;quot;messages&amp;quot;: [{&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: &amp;quot;what is the weather in sf&amp;quot;}]
}

result = agent.invoke(message)

# Print full result (shows all messages + tool calls)
print(&amp;quot;Full result:&amp;quot;, result)

# Print JUST the model's final response
print(&amp;quot;Model response:&amp;quot;, result[&amp;quot;messages&amp;quot;][-1].content)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Surely, I'm finally ready to use this 'quick start tutorial', right?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Model response: There is no weather data for that city.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What!? Okay, I was using Llama 1b. That sad little model probably can't figure out how to use the tool calling in this 'quick start' tutorial. &lt;/p&gt;
&lt;p&gt;Let's try Lama3.2:3b instead. I'll also improve the system prompt and get a lot more specific.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Current weather conditions in San Francisco are not available.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;It must not be using the provided tool. Let me just set a breakpoint. &lt;/p&gt;
&lt;p&gt;Nope, it's using the tool...&lt;/p&gt;
&lt;p&gt;Let's just run it again, just to see what happens:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Model response: I made a mistake! The actual response from the tool is:

&amp;quot;Currently Sunny
Temperature: 62°F
Conditions: Clear
Wind: Light (5 mph)
Sky Conditions: Partly Cloudy&amp;quot;

So, to correct my previous response: The weather in SF is currently sunny with a temperature of 62°F.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Wow, it got the sunny right, finally, but it basically just made the rest of that answer up entirely. Sigh. &lt;/p&gt;
&lt;p&gt;Well, there you go. Here's my final revised 'quick start' code to use LangChain with oLlama. Good luck! (You'll need it.) &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Requires Python 3.10+
# pip install -U langchain langchain-ollama
# Requires Ollama installed + &amp;quot;ollama pull llama3.2&amp;quot;
from langchain_ollama import ChatOllama  # Changed from OpenAI
from langchain.agents import create_agent
from langchain_core.tools import tool

@tool
def get_weather(city: str) -&amp;gt; str:
    &amp;quot;&amp;quot;&amp;quot;Get weather for a given city.&amp;quot;&amp;quot;&amp;quot;
    return f&amp;quot;It's always sunny in {city}!&amp;quot;

# No secrets needed!
llm = ChatOllama(model=&amp;quot;llama3.2:3b&amp;quot;)  # Free local model
agent = create_agent(
    model=llm,
    tools=[get_weather],
    system_prompt=&amp;quot;&amp;quot;&amp;quot;
    You are a weather assistant.
    1. ALWAYS use get_weather tool for weather questions
    2. REPORT EXACTLY what the tool returns - do not make up data
    3. Tool result = actual weather data
    4. Base your answer ONLY on tool output
    &amp;quot;&amp;quot;&amp;quot;,
)

# Run agent
# Our message
message = {
    &amp;quot;messages&amp;quot;: [{&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: &amp;quot;what is the weather in sf&amp;quot;}]
}

result = agent.invoke(message)

# Print full result (shows all messages + tool calls)
print(&amp;quot;Full result:&amp;quot;, result)

# Print JUST the model's final response
print(&amp;quot;Model response:&amp;quot;, result[&amp;quot;messages&amp;quot;][-1].content)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href="https://github.com/brucenielson/LangChainTutorials/blob/51aa88f535ee48160061d44280375ca1cbf5f0fd/simple_example.py"&gt;Github repo found here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/services/artificial-intelligence/"&gt;If you need help with your Artificial Intelligence solutions, we're here to help&lt;/a&gt;.&lt;/p&gt;
</description>
      <pubDate>Fri, 20 Feb 2026 12:00:00 -0700</pubDate>
      <a10:updated>2026-02-20T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2694</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/weekend-warrior-project-ai-patent-checking/</link>
      <category>System.String[]</category>
      <title>Weekend Warrior Project: AI Patent Checking</title>
      <description>&lt;p&gt;Ever had an idea and wondered, &lt;em&gt;“Wait… has someone already patented this?”&lt;/em&gt; At Mindfire Tech, we recently faced that exact problem. We were working on an AI patent, and before we could go any further, we needed to know if the concept was already patented.&lt;/p&gt;
&lt;p&gt;Sure, we could have spent hours — maybe days — digging through USPTO records, manually reading abstracts, and trying to make sense of hundreds of documents. Or… we could let AI do it for us.  
&lt;/p&gt;
&lt;p&gt;One weekend later, we had a fully functioning AI Patent Search agent. In a few hours, we built a tool where you just paste in your patent abstract (or the full patent!), and it will:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate intelligent search queries.&lt;/li&gt;
&lt;li&gt;Search the USPTO patent database automatically. &lt;/li&gt;
&lt;li&gt;Refine queries if there are too many or too few results.  
&lt;/li&gt;
&lt;li&gt;Retrieve abstracts and documents.  
&lt;/li&gt;
&lt;li&gt;Rank the results by relevance.  
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All you have to do is review the final top results — the agent does the rest. The funniest part? Writing the AI agent was &lt;em&gt;far easier&lt;/em&gt; than trying to do this work manually. Instead of getting lost in endless PDFs and XML files, we spent the weekend building a tool that could save weeks of research for anyone working on patents.  
&lt;/p&gt;
&lt;p&gt;It’s the kind of “weekend warrior” project that feels like magic: a fully functional AI agent built in a single weekend, solving a problem that used to take days. At Mindfire, that’s exactly the kind of creative, high-impact AI work we love doing for our clients.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/screenshot-2026-01-14-204551-1.png" alt="Mindfire's Patent Search" /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;How It Works: The Tech Behind the Magic&lt;/h2&gt;
&lt;p&gt;While the AI agent feels seamless, there’s a lot going on under the hood. Here’s a breakdown of the technologies that make this possible:&lt;/p&gt;
&lt;h3&gt;1. Gradio for a Clean, Instant UI&lt;/h3&gt;
&lt;p&gt;We used &lt;strong&gt;Gradio&lt;/strong&gt;, a Python library, to create a web interface in minutes.  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Textbox for your patent idea  
&lt;/li&gt;
&lt;li&gt;Dropdown to select an AI model  
&lt;/li&gt;
&lt;li&gt;Button to trigger the search  
&lt;/li&gt;
&lt;li&gt;Expandable previews of retrieved patents  
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All interactive, all local — no frontend frameworks, no headaches.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;2. Local LLMs with Ollama&lt;/h3&gt;
&lt;p&gt;All AI reasoning happens locally using &lt;strong&gt;Ollama&lt;/strong&gt;.  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Models like Gemma, Llama, and Mistral run right on your machine  
&lt;/li&gt;
&lt;li&gt;No data leaves your system — perfect for confidential IP  
&lt;/li&gt;
&lt;li&gt;Fast enough to generate search queries and semantic rankings in seconds  
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We used Ollama as the backbone for our AI agent, orchestrated with DSPy.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;3. DSPy: Structured AI Orchestration&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;DSPy&lt;/strong&gt; provides the “brains” of the agent:  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generates structured queries from your patent idea  
&lt;/li&gt;
&lt;li&gt;Refines queries if the first pass fails  
&lt;/li&gt;
&lt;li&gt;Scores patents for relevance using their abstracts  
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;DSPy enforces strict input/output formats, which keeps the AI’s predictions consistent and reliable. It’s what turns a raw LLM into a real research assistant.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;4. Multi-Stage Patent Retrieval&lt;/h3&gt;
&lt;p&gt;The agent searches the USPTO using our &lt;strong&gt;Python wrapper for their API&lt;/strong&gt;:  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Submits structured keyword queries  
&lt;/li&gt;
&lt;li&gt;Filters only granted patents  
&lt;/li&gt;
&lt;li&gt;Fetches up to hundreds of results per query  
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a query returns zero or too many results, the agent automatically refines it.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;5. FlashRank Reranking&lt;/h3&gt;
&lt;p&gt;Once we have raw results, we use &lt;strong&gt;FlashRank&lt;/strong&gt;, a fast CPU-based reranker:  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scores patents by relevance to your idea  
&lt;/li&gt;
&lt;li&gt;Works without GPUs or PyTorch  
&lt;/li&gt;
&lt;li&gt;Reduces hundreds of results to the most relevant top candidates  
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This ensures you only see what really matters.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;6. Full Patent Document Retrieval&lt;/h3&gt;
&lt;p&gt;For the top patents, the agent fetches the &lt;strong&gt;full XML from the USPTO&lt;/strong&gt;:  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extracts abstracts with &lt;code&gt;lxml&lt;/code&gt;  
&lt;/li&gt;
&lt;li&gt;Prepares server-side previews for your review  
&lt;/li&gt;
&lt;li&gt;Keeps everything local and ready for the next step  
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No more opening PDFs one by one — the agent handles the tedious part.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;7. Semantic Reranking with LLMs&lt;/h3&gt;
&lt;p&gt;Finally, the agent uses the LLM again to read and rank abstracts semantically:  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scores each abstract 0–1 for relevance  
&lt;/li&gt;
&lt;li&gt;Sorts patents so the top few are the ones you actually care about  
&lt;/li&gt;
&lt;li&gt;Provides a concise, actionable list of results, including the actual abstracts. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes the final output feel like a real research assistant — not just a search engine.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;The Result&lt;/h3&gt;
&lt;p&gt;In a single weekend, we created an agent that:  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understands your patent idea  
&lt;/li&gt;
&lt;li&gt;Queries official government databases  
&lt;/li&gt;
&lt;li&gt;Retrieves, parses, and ranks hundreds of patents  
&lt;/li&gt;
&lt;li&gt;Presents a clean, interactive review interface  
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s fast, reliable, and ready to save anyone hours of manual research.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;AI Patent Search Agent Pipeline&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/pipeline.png" alt="Patent AI Agent Pipeline" /&gt;&lt;/p&gt;
&lt;p&gt;The diagram above shows how the agent flows from idea input to semantic scoring, highlighting all the key stages of retrieval, refinement, and ranking.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Why Mindfire Can Help&lt;/h2&gt;
&lt;p&gt;Building an AI prototype in a weekend is fun — but imagine what our team can do with more time and your unique business challenges.  
&lt;/p&gt;
&lt;p&gt;At Mindfire Tech, we specialize in:  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom AI agents for research and analysis  
&lt;/li&gt;
&lt;li&gt;Automation that saves weeks of manual work  
&lt;/li&gt;
&lt;li&gt;Secure, locally hosted AI solutions for sensitive projects  
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you’re curious about how AI can transform your workflows — patent research or beyond — we’d love to talk.  
&lt;/p&gt;
&lt;p&gt;After all, if we can build a full patent search agent in a weekend, imagine what we could build for your business.&lt;/p&gt;
</description>
      <pubDate>Fri, 06 Feb 2026 12:00:00 -0700</pubDate>
      <a10:updated>2026-02-06T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2699</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/from-prompts-to-applications-a-beginner-s-introduction-to-langchain/</link>
      <category>System.String[]</category>
      <title>From Prompts to Applications: A Beginner’s Introduction to LangChain</title>
      <description>&lt;p&gt;If you’ve spent any time experimenting with large language models, you’ve probably had this experience: the first few prompts feel magical — and then things get messy and difficult. You want your model to look things up, remember context, call tools, or follow a multi-step process. Suddenly, a single prompt isn’t enough. And those dang Large Language Models (LLMs) seem to have a mind of their own. &lt;/p&gt;
&lt;p&gt;That’s the gap &lt;a href="https://www.langchain.com/"&gt;LangChain&lt;/a&gt; was created to fill.&lt;/p&gt;
&lt;p&gt;LangChain is an open-source framework designed to help developers move from isolated LLM calls to structured AI applications. Instead of treating a language model as a black box that simply returns text, LangChain encourages you to think in terms of workflows — sequences of steps that combine models, tools, and data sources into something more reliable and reusable. This philosophy is central to how the project describes itself and why it exists (&lt;a href="https://docs.langchain.com/oss/python/langchain/philosophy"&gt;LangChain Philosophy&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;What LangChain Actually Is&lt;/h2&gt;
&lt;p&gt;At a high level, LangChain provides abstractions for working with language models in a consistent way. It standardizes how you connect to different model providers and how models interact with external systems. The core idea is simple but powerful: Models should be used for more than just text generation - they should also be used to orchestrate more complex flows that interact with other data. (See &lt;a href="https://docs.langchain.com/oss/python/langchain/philosophy"&gt;LangChain Philosophy&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;This shows up in LangChain’s building blocks. You’ll often hear terms like &lt;em&gt;chains&lt;/em&gt;, &lt;em&gt;tools&lt;/em&gt;, and &lt;em&gt;agents&lt;/em&gt;. Chains represent ordered steps of computation, tools allow models to interact with external data or APIs, and agents enable models to decide dynamically which tools to use. Together, these concepts make it easier to build things like retrieval-augmented generation (RAG), multi-step reasoning pipelines, and AI assistants that can interact with real-world data.&lt;/p&gt;
&lt;h2&gt;Installing LangChain&lt;/h2&gt;
&lt;p&gt;LangChain is intentionally easy to get started with, especially for Python developers. Installation typically starts with the core package, followed by optional integrations for the model providers you plan to use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install -U langchain
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Or do a provider specific version like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install -U langchain-openai
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Or (better yet for us low cost AI solution types) this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install -U langchain langchain-ollama
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This modular approach lets developers start small and only pull in what they need, while still leaving room to grow into more advanced use cases later (see the official &lt;a href="https://docs.langchain.com/oss/python/langchain/install"&gt;LangChain Installation Docs&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;A Quick Start Example&lt;/h2&gt;
&lt;p&gt;This short quick start tutorial should get you started:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from langchain_ollama import ChatOllama
from langchain_core.messages import HumanMessage

# Initialize the Ollama model
llm = ChatOllama(model=&amp;quot;llama3.2:1b&amp;quot;)

# Create a human message
message = HumanMessage(content=&amp;quot;Write a short introduction about LangChain&amp;quot;)

# Generate a response
response = llm.generate([[message]])

# The response object contains generations
print(response.generations[0][0].text)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You will have to have &lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-ollama-for-large-language-models-llm-in-windows/"&gt;&lt;strong&gt;ollama already installed&lt;/strong&gt;&lt;/a&gt; and llama3.2:1b downloaded. &lt;/p&gt;
&lt;h2&gt;When LangChain Shines&lt;/h2&gt;
&lt;p&gt;LangChain works best when your application needs more than a single prompt and response. If you’re building workflows that involve retrieving information from documents, calling external APIs, coordinating multiple LLM calls, or guiding a model through a multi-step reasoning process, LangChain provides helpful structure without forcing a rigid architecture.&lt;/p&gt;
&lt;p&gt;It is especially popular as a prototyping and experimentation tool. Many developers use LangChain to explore ideas like agent-based workflows or retrieval-augmented generation (RAG) systems before deciding how much structure they want to carry forward into production. Examples like agentic GraphRAG systems built with LangChain show how it can connect models, tools, and data sources in flexible ways (&lt;a href="https://ai.gopubby.com/agentic-graphrag-with-neo4j-and-langchain-ce03b344149c"&gt;Agentic GraphRAG with LangChain&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;When to Be Careful&lt;/h2&gt;
&lt;p&gt;Despite its popularity, LangChain is not universally considered production-ready out of the box. Some developers argue that its abstractions can introduce unnecessary complexity, make debugging harder, or obscure performance characteristics in real-world systems. This concern comes up frequently in discussions about production RAG pipelines, where fine-grained control over retrieval logic, latency, and observability is often critical (&lt;a href="https://medium.com/@aldendorosario/langchain-is-not-for-production-use-here-is-why-9f1eca6cce80"&gt;LangChain Is Not for Production Use — Here Is Why&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;This doesn’t mean LangChain shouldn’t be used at all — but it does mean it should be treated as a toolkit rather than a complete solution. Strong engineering practices are still required to turn prototypes into reliable, maintainable applications.&lt;/p&gt;
&lt;h2&gt;LangChain in the Broader Ecosystem&lt;/h2&gt;
&lt;p&gt;LangChain exists alongside several other frameworks with overlapping goals. Tools like Haystack focus more heavily on search and retrieval performance, while newer projects like LangGraph emphasize lower-level control over agent workflows. Which framework makes sense depends largely on your use case, performance requirements, and tolerance for abstraction (&lt;a href="https://medium.com/@amit25173/langchain-vs-haystack-7fa0faa901cd"&gt;LangChain vs Haystack&lt;/a&gt;, &lt;a href="https://medium.com/data-science/ai-agent-workflows-a-complete-guide-on-whether-to-build-with-langgraph-or-langchain-117025509fa0"&gt;LangGraph vs LangChain&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;LangChain is best understood as a bridge. It connects the early excitement of prompt-based experimentation with the practical reality of building real AI applications. For beginners, it offers a way to think beyond prompts and start designing systems composed of models, tools, and workflows. Used thoughtfully, LangChain can be an effective stepping stone from experimentation to production — as long as its tradeoffs are clearly understood.&lt;/p&gt;
</description>
      <pubDate>Fri, 30 Jan 2026 12:00:00 -0700</pubDate>
      <a10:updated>2026-01-30T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2692</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/ai-bubble-or-breakthrough-a-look-at-the-risks-and-the-rewards/</link>
      <category>System.String[]</category>
      <title>AI: Bubble or Breakthrough? A Look at the Risks and the Rewards</title>
      <description>&lt;p&gt;Artificial intelligence is arguably &lt;em&gt;the most hyped technology in decades&lt;/em&gt;. But hype isn’t the same as reality. In the debate over whether AI is in a bubble, there is both a pessimistic and a more optimistic view. &lt;strong&gt;Cory Doctorow&lt;/strong&gt; warns that the economic foundations of AI are shaky and potentially disastrous, and &lt;strong&gt;Jeff Bezos&lt;/strong&gt;, who agrees there’s a bubble — but insists it’s the &lt;em&gt;good kind&lt;/em&gt; and heralds long-term benefits.&lt;/p&gt;
&lt;p&gt;Let’s explore both sides with concrete examples.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Pessimistic View: AI as “Funny Money” and Fragile Economics&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://doctorow.medium.com/https-pluralistic-net-2025-09-27-econopocalypse-subprime-intelligence-e9a06136d109"&gt;Cory Doctorow’s article &lt;em&gt;“The real (economic) AI apocalypse is nigh”&lt;/em&gt;&lt;/a&gt; argues that &lt;strong&gt;today’s AI bubble isn’t built on sound economics&lt;/strong&gt; but on massive investor excitement and weak financial fundamentals. He paints a picture of an industry where &lt;em&gt;capital flows in without sustainable revenue models or profit paths&lt;/em&gt; — classic signs of a bubble that could end badly.&lt;/p&gt;
&lt;p&gt;Doctorow argues many AI firms “&lt;em&gt;keep the lights on by soaking up hundreds of billions of dollars in other people’s money and then lighting it on fire.&lt;/em&gt;” He argues it’s a dependency on the continual infusion of new capital, hardly a sustainable business model.&lt;/p&gt;
&lt;p&gt;Doctorow points out that, unlike past emerging technologies, &lt;strong&gt;each generation of AI technology costs more to build and operate than the last&lt;/strong&gt;, with little evidence that those costs decline or that scaling will improve profitability.&lt;/p&gt;
&lt;p&gt;He also points out that some AI data center companies are reportedly &lt;em&gt;collateralizing loans with tens of thousands of GPUs&lt;/em&gt;, despite these chips losing value quickly — a bizarre financial setup that signals desperation rather than strength.&lt;/p&gt;
&lt;p&gt;Even scarier is where Doctorow highlights practices where &lt;strong&gt;the same money is booked as an investment by one company and revenue by another&lt;/strong&gt; — such as Microsoft “investing” in OpenAI by providing server access, then counting that as revenue. That’s &lt;em&gt;not&lt;/em&gt; real earnings; it’s accounting sleight-of-hand.&lt;/p&gt;
&lt;p&gt;Finally, he quotes a venture capitalist suggesting AI firms would need to &lt;em&gt;sell hundreds of billions worth of services just to break even&lt;/em&gt;, a goal so large it’s hard to take seriously given current revenue levels. Is there really this much demand for AI in real life?&lt;/p&gt;
&lt;p&gt;Doctorow's view is that this whole thing is just hype run out of control, leading to bad decisions—such as firing people over AI that can't do the job. In Doctorow's opinion, this is just the market gone mad.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The Optimistic Take: Jeff Bezos on the “Industrial Bubble”&lt;/h2&gt;
&lt;p&gt;But not everyone sees this as &lt;em&gt;pure&lt;/em&gt; madness. Jeff Bezos — longtime tech leader and founder of Amazon — also acknowledges we’re in &lt;em&gt;bubble territory&lt;/em&gt;, but his perspective is more nuanced.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=4Vf8pljp1FY"&gt;Bezos said at &lt;em&gt;Italian Tech Week 2025&lt;/em&gt;&lt;/a&gt; that &lt;strong&gt;AI’s rapid growth and inflated valuations fit the classic pattern of a bubble&lt;/strong&gt;, where “every experiment and every company gets funded — the good ideas and the bad ones alike.” &lt;/p&gt;
&lt;p&gt;Bezos compared the AI bubble to past tech booms particularly the dot-com bubble. During the dot-com bust, Amazon’s stock famously plummeted from roughly &lt;em&gt;$113 to around $6 a share&lt;/em&gt; during the early 2000s bust, even as its business fundamentals were improving. This shows how markets can wildly misprice companies in a bubble. &lt;/p&gt;
&lt;p&gt;Bezos also points out that during the dot-com bubble everyone was chasing the Internet. Fiber-optic companies went bankrupt laying fiber, but the fiber they laid was bought out by other companies and is still around. The overall benefit to society was real. &lt;/p&gt;
&lt;p&gt;This actually makes sense. Everyone can sense that AI is a transformative technology just like the Internet was. Back then everyone was trying to figure out how to use (and preferably dominate) the Internet because they knew there was massive profits on the line. So, from an investor's perspective, it made sense to chase these profits even if you risked losing everything. And for the companies that won this mad dash—Amazon, Google, Netflix, Facebook, etc—the rewards were well worth the risk. &lt;/p&gt;
&lt;p&gt;Notice how this doesn't deny the problems Doctorow highlights, but it explains it differently. Instead of trying to explain it as market madness, it explains it as a correct understanding that huge profits are on the line. The problem is that no one knows at this point who the big winners are going to be. &lt;/p&gt;
&lt;p&gt;This is what Bezos calls an &lt;em&gt;industrial bubble&lt;/em&gt;: &lt;strong&gt;capital may flow to losers, but the underlying technology creates lasting value&lt;/strong&gt;. Compare this to the bad kind of bubble, such as the 2008 housing bubble. That bubble was all bad because it really was just market madness with on actual paradigm-changing technology at its center.&lt;/p&gt;
&lt;p&gt;Bezos points out that during hype cycles, &lt;strong&gt;investors often can’t distinguish good ideas from bad ones&lt;/strong&gt; — meaning capital pours into projects that may never have viable products. But in the process, &lt;em&gt;real innovation also gets funded&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Importantly, Bezos argues that &lt;em&gt;just because valuations are frothy doesn’t mean the technology itself isn’t transformative&lt;/em&gt;. The dot-com bubble was a bubble, but the Internet really did transform everything. So, Bezos maintains that &lt;strong&gt;AI will change every industry&lt;/strong&gt; and that the benefits to society will be “gigantic.” &lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;So What Should We Make of It?&lt;/h2&gt;
&lt;h3&gt;🚨 The Bubble Side&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;AI has &lt;strong&gt;massive investment without clear returns&lt;/strong&gt;. &lt;/li&gt;
&lt;li&gt;Financial engineering and accounting quirks mask true economics.  
&lt;/li&gt;
&lt;li&gt;If capital stops flowing, many companies might fail spectacularly.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;🌟 The Breakthrough Side&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Innovation doesn’t happen without experimentation and funding&lt;/strong&gt;, even if it’s wasteful at times.&lt;/li&gt;
&lt;li&gt;Past &lt;em&gt;industrial bubbles&lt;/em&gt; (Internet) left real infrastructure and products behind. &lt;/li&gt;
&lt;li&gt;AI’s underlying &lt;em&gt;technical progress&lt;/em&gt; isn’t a mirage — it’s real and pushing boundaries.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;So I Should Be Afraid of the AI Bubble?&lt;/h2&gt;
&lt;p&gt;Surprisingly, no — though, you probably &lt;em&gt;should&lt;/em&gt; be concerned. Take a look at this chart of the Shiller PE ratio and our three bubbles:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/shiller-pe.png" alt="Three Bubbles" /&gt;&lt;/p&gt;
&lt;p&gt;A Shiller PE is a price-to-earnings ratio, except smoothed out over 10 years. It's a great way to fundamentally value the market historically that smooths out all the local ups and downs. During the dot-com bubble the Shiller PE got up to 44 against a historical average of closer to 20. We are currently at 40. Even the 2008 housing bubble only got up to 28. So there is a lot of potential downside in this AI bubble. The stock market is clearly overvalued in large part due to the AI bubble.&lt;/p&gt;
&lt;h2&gt;Conclusion: Bubble or Breakthrough?&lt;/h2&gt;
&lt;p&gt;The answer might be &lt;strong&gt;both&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;AI today shows many signs of a bubble: unsustainable valuations, weak unit economics, and overfunded startups with no path to profit. That’s worth worry — especially for investors and workers whose livelihoods depend on sound business fundamentals.&lt;/p&gt;
&lt;p&gt;But if we look at history, &lt;em&gt;bubbles aren’t uniformly bad&lt;/em&gt;. As Jeff Bezos emphasizes, when the dust settles, the infrastructure and breakthroughs left behind can help reshape industries for decades.&lt;/p&gt;
&lt;p&gt;Whether you come down on the &lt;em&gt;doom&lt;/em&gt; side or the &lt;em&gt;long-term value&lt;/em&gt; side, one thing is clear: &lt;strong&gt;AI’s story is just beginning, and its economic impact will be debated for years to come&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In the meantime, one value that can be certain from AI is its use in projects and software that have an immediate purpose and measurable benefit or profitability. Mindfire Tech is committed to providing state-of-the-art software solutions and we believe AI can be a great boon to those who know how to use it properly. If you are interested in learning more about how AI could be used in your company or in your future projects, &lt;a href="https://www.mindfiretechnology.com/contact-us"&gt;please don't hesitate to reach out for a free consultation&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Sources:&lt;/em&gt;&lt;br /&gt;
- Cory Doctorow, &lt;em&gt;The real (economic) AI apocalypse is nigh&lt;/em&gt; — Medium. :contentReference[oaicite:18]{index=18}&lt;br /&gt;
- Jeff Bezos on the AI bubble, Italian Tech Week 2025 — various reporting. :contentReference[oaicite:19]{index=19}&lt;/p&gt;
</description>
      <pubDate>Tue, 20 Jan 2026 12:00:00 -0700</pubDate>
      <a10:updated>2026-01-20T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2688</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/building-a-local-deepseek-r1-chatbot-with-chainlit/</link>
      <category>System.String[]</category>
      <title>Building a Local DeepSeek R1 Chatbot with Chainlit</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/building-a-local-deepseek-r1-chatbot-with-streamlit-and-ollama/"&gt;In the previous post&lt;/a&gt; we used &lt;a href="https://streamlit.io/"&gt;Streamlit&lt;/a&gt; and &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt; to build a local Deepseek R1 chatbot. Let's now do the same thing using &lt;a href="https://docs.chainlit.io/get-started/overview"&gt;Chainlit&lt;/a&gt; as our UI to try out Chainlit. Chainlit provides an elegant, real-time chat interface out of the box, and it works beautifully with models running through &lt;strong&gt;Ollama&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Just like before, DeepSeek R1 will stream both its &lt;em&gt;thinking process&lt;/em&gt; and its final answer. Chainlit is dedicated specifically to building chatbots and doesn't seem quite as flexible as Streamlit. This led to at least one problem I'll explain below.&lt;/p&gt;
&lt;p&gt;The full working code is included in this post and &lt;a href="https://github.com/brucenielson/PythonUIs/blob/2f8eb60bb97350ce3e5a2b22d05520f624aaa692/chainlit_example.py"&gt;in my GitHub repo&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What You'll Need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Python 3.11+ installed  
&lt;/li&gt;
&lt;li&gt;Ollama installed (&lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-ollama-for-large-language-models-llm-in-windows/"&gt;see this post&lt;/a&gt;)  
&lt;/li&gt;
&lt;li&gt;DeepSeek R1 pulled locally:&lt;br /&gt;
  &lt;code&gt;ollama pull deepseek-r1:1.5b&lt;/code&gt;  
&lt;/li&gt;
&lt;li&gt;Chainlit:&lt;br /&gt;
  &lt;code&gt;pip install chainlit&lt;/code&gt;  
&lt;/li&gt;
&lt;li&gt;Ollama Python library:&lt;br /&gt;
  &lt;code&gt;pip install ollama&lt;/code&gt;  
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Complete Code&lt;/h2&gt;
&lt;p&gt;Below is the full &lt;code&gt;app.py&lt;/code&gt; file we'll walk through:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import chainlit as cl
import ollama


def convert_latex_delimiters(text):
    &amp;quot;&amp;quot;&amp;quot;Convert LaTeX delimiters from backslash-bracket to dollar signs&amp;quot;&amp;quot;&amp;quot;
    if not text:
        return text
    # Replace display math delimiters
    text = text.replace(r'\[', '$$')
    text = text.replace(r'\]', '$$')
    # Replace inline math delimiters
    text = text.replace(r'\(', '$')
    text = text.replace(r'\)', '$')
    return text


@cl.on_message
async def on_message(message: cl.Message):
    &amp;quot;&amp;quot;&amp;quot;
    Handles incoming messages from the user, sends them to the LLM,
    and streams the response back to the Chainlit interface with thinking process.
    &amp;quot;&amp;quot;&amp;quot;
    # System prompt for the AI
    system_message = {
        &amp;quot;role&amp;quot;: &amp;quot;system&amp;quot;,
        &amp;quot;content&amp;quot;: &amp;quot;&amp;quot;&amp;quot;You are an advanced AI assistant powered by the deepseek-r1 model.

Guidelines:
- If you're uncertain about something, acknowledge it rather than making up information
- Format your responses with markdown when it improves readability
&amp;quot;&amp;quot;&amp;quot;
    }

    # Create a step for thinking and messages for streaming
    thinking_step = cl.Step(name=&amp;quot;💭 Thinking&amp;quot;, type=&amp;quot;tool&amp;quot;)
    final_answer = cl.Message(content=&amp;quot;&amp;quot;)

    accumulated_thinking = &amp;quot;&amp;quot;
    accumulated_answer = &amp;quot;&amp;quot;

    try:
        # Request completion from the model with streaming and thinking enabled
        stream = ollama.chat(
            model=&amp;quot;deepseek-r1:1.5b&amp;quot;,
            messages=[
                system_message,
                {&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: message.content}
            ],
            stream=True,
            think=True,  # This is the critical parameter for Ollama native API
        )

        thinking_started = False
        answer_started = False
        answer_buffer = &amp;quot;&amp;quot;

        # Stream the response to the UI
        for chunk in stream:
            chunk_msg = chunk.get(&amp;quot;message&amp;quot;, {})

            # Handle thinking content
            if chunk_msg.get(&amp;quot;thinking&amp;quot;):
                if not thinking_started:
                    thinking_started = True
                    await thinking_step.send()

                thinking_text = chunk_msg[&amp;quot;thinking&amp;quot;]
                accumulated_thinking += thinking_text
                thinking_step.output = convert_latex_delimiters(accumulated_thinking)
                await thinking_step.update()

            # Handle answer content
            if chunk_msg.get(&amp;quot;content&amp;quot;):
                if not answer_started:
                    answer_started = True
                    if thinking_started:
                        # Finalize the thinking step
                        await thinking_step.update()
                    await final_answer.send()

                answer_text = chunk_msg[&amp;quot;content&amp;quot;]
                accumulated_answer += answer_text
                answer_buffer += answer_text

                # Only update every 10 characters or so to avoid overwhelming the socket
                if len(answer_buffer) &amp;gt;= 10:
                    await final_answer.stream_token(answer_buffer)
                    answer_buffer = &amp;quot;&amp;quot;

        # Send any remaining buffered content
        if answer_buffer:
            await final_answer.stream_token(answer_buffer)

        # Update final answer with LaTeX conversion
        final_answer.content = convert_latex_delimiters(accumulated_answer)
        await final_answer.update()

    except Exception as e:
        error_msg = cl.Message(content=f&amp;quot;❌ Error generating response: {str(e)}&amp;quot;)
        await error_msg.send()


@cl.on_chat_start
async def start():
    &amp;quot;&amp;quot;&amp;quot;
    Sends a welcome message when the chat starts.
    &amp;quot;&amp;quot;&amp;quot;
    await cl.Message(
        content=&amp;quot;👋 Hello! I'm powered by **DeepSeek R1**. I'll show you my thinking process before answering.\n\n&amp;quot;
                &amp;quot;Try asking me a math problem or reasoning question!&amp;quot;
    ).send()
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Breaking Down the Code&lt;/h2&gt;
&lt;h3&gt;Chainlit Event Hooks&lt;/h3&gt;
&lt;p&gt;Chainlit uses decorators such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@cl.on_message
@cl.on_chat_start
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;These act as event listeners.&lt;br /&gt;
- &lt;code&gt;on_chat_start&lt;/code&gt; fires once when the UI loads.&lt;br /&gt;
- &lt;code&gt;on_message&lt;/code&gt; fires every time the user sends a message.&lt;/p&gt;
&lt;h3&gt;LaTeX Conversion Helper&lt;/h3&gt;
&lt;p&gt;DeepSeek R1 frequently uses LaTeX but with delimiters that Chainlit doesn't render by default. We fix that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def convert_latex_delimiters(text):
    text = text.replace(r'\[', '$$')
    text = text.replace(r'\]', '$$')
    text = text.replace(r'\(', '$')
    text = text.replace(r'\)', '$')
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Same logic as the Streamlit version, just applied before displaying anything.&lt;/p&gt;
&lt;h3&gt;Thinking Step&lt;/h3&gt;
&lt;p&gt;Chainlit allows you to create &amp;quot;steps&amp;quot; that appear in the UI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;thinking_step = cl.Step(name=&amp;quot;💭 Thinking&amp;quot;, type=&amp;quot;tool&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As the model streams reasoning content, we update this step live. The user can watch DeepSeek R1 deliberate. &lt;/p&gt;
&lt;p&gt;Unfortunately, I couldn't figure out (in time for this post) how to get the thinking to sit on top of the answer. So if you are watching it think the spot where the final answer goes scrolls off the top of the screen. I tried several ideas on how to fix this and none worked. Good job Chainlit getting your app that has only one job to not do it right! (At least not easily.) &lt;/p&gt;
&lt;h3&gt;Streaming with Ollama&lt;/h3&gt;
&lt;p&gt;The heart of the system:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stream = ollama.chat(
    model=&amp;quot;deepseek-r1:1.5b&amp;quot;,
    messages=[...],
    stream=True,
    think=True,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stream=True&lt;/code&gt; → tokens arrive as a generator  
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;think=True&lt;/code&gt; → reasoning is delivered separately from the final answer  
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Streaming Back to Chainlit&lt;/h3&gt;
&lt;p&gt;We listen for both thinking and final answer tokens:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if chunk_msg.get(&amp;quot;thinking&amp;quot;):
    ...
if chunk_msg.get(&amp;quot;content&amp;quot;):
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Thinking updates go to &lt;code&gt;thinking_step.update()&lt;/code&gt;, while answer tokens are streamed via:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await final_answer.stream_token(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This gives a smooth, real-time chat experience.&lt;/p&gt;
&lt;h3&gt;Welcome Message&lt;/h3&gt;
&lt;p&gt;When the chat starts, we show a friendly introduction:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await cl.Message(
    content=&amp;quot;👋 Hello! I'm powered by **DeepSeek R1**...&amp;quot;
).send()
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Error Handling&lt;/h3&gt;
&lt;p&gt;Any unexpected issues are passed back to the UI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;await cl.Message(content=f&amp;quot;❌ Error generating response: {str(e)}&amp;quot;).send()
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Running the Application&lt;/h2&gt;
&lt;p&gt;Save the file as &lt;code&gt;app.py&lt;/code&gt; and run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chainlit run chainlit_example.py -w
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Your browser will open automatically with the Chainlit interface. Try a math or logic problem, and you'll see DeepSeek R1 stream its thoughts step-by-step before answering.&lt;/p&gt;
&lt;p&gt;Final Result:
&lt;img src="https://www.mindfiretechnology.com/blog/media/screenshot-2025-12-04-174722.png" alt="enter image description here" /&gt;&lt;/p&gt;
&lt;h2&gt;Key Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Runs entirely locally—no external API calls  
&lt;/li&gt;
&lt;li&gt;Chainlit displays the reasoning process as a dedicated live-updating step  
&lt;/li&gt;
&lt;li&gt;Smooth streaming output  
&lt;/li&gt;
&lt;li&gt;Proper math rendering  
&lt;/li&gt;
&lt;li&gt;Clean and modern chat UI with minimal setup  
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Concerns&lt;/h2&gt;
&lt;p&gt;I never did get Chainlit to work as well as Streamlit. It tended to error out due to 'too many tokens' errors and the Latex support was hit and miss at best. I'll have to circle back on this and see if I can improve it.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Using Chainlit with DeepSeek R1 makes it incredibly simple to build a polished chatbot interface. With around 100 lines of code, you get live thinking visualization, Markdown rendering, history management, and a production-ready UI—all running locally via Ollama.&lt;/p&gt;
&lt;p&gt;Feel free to adapt this example and extend it for your own experiments!&lt;/p&gt;
</description>
      <pubDate>Tue, 06 Jan 2026 12:00:00 -0700</pubDate>
      <a10:updated>2026-01-06T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2685</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/building-a-local-deepseek-r1-chatbot-with-streamlit-and-ollama/</link>
      <category>System.String[]</category>
      <title>Building a Local DeepSeek R1 Chatbot with Streamlit and Ollama</title>
      <description>&lt;p&gt;With all the -- admittedly already out of date -- hype around &lt;a href="https://www.mindfiretechnology.com/blog/archive/explaining-deepseek-r1-and-how-to-use-it/"&gt;Deepseek R1&lt;/a&gt; from China, I thought it would be fun to build a simple chatbot interface for DeepSeek R1 that runs entirely on your local machine. DeepSeek R1 is a reasoning model that shows its &amp;quot;thinking process&amp;quot; before giving answers, similar to OpenAI's o1 model. We'll use &lt;a href="https://streamlit.io/"&gt;Streamlit for the interface&lt;/a&gt; and &lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-ollama-for-large-language-models-llm-in-windows/"&gt;Ollama to run the model locally&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The full code available both in this post and in &lt;a href="https://github.com/brucenielson/PythonUIs/blob/f19932dfe243dc803eb0ae2de9f3b693d133164d/streamlit_example.py"&gt;my Github Repo found here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What You'll Need&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Python 3.11+ installed on your computer&lt;/li&gt;
&lt;li&gt;Ollama installed (&lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-ollama-for-large-language-models-llm-in-windows/"&gt;see this post for details&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The DeepSeek R1 model pulled in Ollama: &lt;code&gt;ollama pull deepseek-r1:1.5b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Streamlit: &lt;code&gt;pip install streamlit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Ollama Python library: &lt;code&gt;pip install ollama&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The Complete Code&lt;/h2&gt;
&lt;p&gt;Here's the full code we'll be breaking down:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Run using: streamlit run streamlit_example.py
import streamlit as st
import ollama

st.set_page_config(page_title=&amp;quot;DeepSeek R1 Chat&amp;quot;, page_icon=&amp;quot;🤖&amp;quot;)


def convert_latex_delimiters(text):
    &amp;quot;&amp;quot;&amp;quot;Convert LaTeX delimiters from backslash-bracket to dollar signs&amp;quot;&amp;quot;&amp;quot;
    # Replace display math delimiters
    text = text.replace(r'\[', '$$')
    text = text.replace(r'\]', '$$')
    # Replace inline math delimiters
    text = text.replace(r'\(', '$')
    text = text.replace(r'\)', '$')
    return text


# Initialize chat history
if &amp;quot;messages&amp;quot; not in st.session_state:
    st.session_state.messages = []

# Display chat history
for msg in st.session_state.messages:
    with st.chat_message(msg[&amp;quot;role&amp;quot;]):
        if msg[&amp;quot;role&amp;quot;] == &amp;quot;assistant&amp;quot; and msg.get(&amp;quot;thinking&amp;quot;):
            with st.expander(&amp;quot;🧠 View Thinking Process&amp;quot;, expanded=False):
                st.markdown(convert_latex_delimiters(msg[&amp;quot;thinking&amp;quot;]))
        st.markdown(convert_latex_delimiters(msg[&amp;quot;content&amp;quot;]))

# Chat input
user_input = st.chat_input(&amp;quot;Ask DeepSeek R1&amp;quot;)

if user_input:
    # Add and display user message
    st.session_state.messages.append({&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: user_input})
    with st.chat_message(&amp;quot;user&amp;quot;):
        st.markdown(user_input)

    # Generate and display assistant response
    with st.chat_message(&amp;quot;assistant&amp;quot;):
        thinking_display = st.empty()
        answer_display = st.empty()
        accumulated_thinking = &amp;quot;&amp;quot;
        accumulated_answer = &amp;quot;&amp;quot;

        with st.spinner(&amp;quot;Thinking...&amp;quot;, show_time=True):
            stream = ollama.chat(
                model=&amp;quot;deepseek-r1:1.5b&amp;quot;,
                messages=[{&amp;quot;role&amp;quot;: m[&amp;quot;role&amp;quot;], &amp;quot;content&amp;quot;: m[&amp;quot;content&amp;quot;]}
                          for m in st.session_state.messages],
                stream=True,
                think=True,
            )

            for chunk in stream:
                chunk_msg = chunk.get(&amp;quot;message&amp;quot;, {})

                # Stream thinking content
                if chunk_msg.get(&amp;quot;thinking&amp;quot;):
                    accumulated_thinking += chunk_msg[&amp;quot;thinking&amp;quot;]
                    with thinking_display.container():
                        with st.expander(&amp;quot;🧠 View Thinking Process&amp;quot;, expanded=True):
                            st.markdown(convert_latex_delimiters(accumulated_thinking) + &amp;quot;▌&amp;quot;)

                # Stream answer content
                if chunk_msg.get(&amp;quot;content&amp;quot;):
                    accumulated_answer += chunk_msg[&amp;quot;content&amp;quot;]
                    answer_display.markdown(convert_latex_delimiters(accumulated_answer) + &amp;quot;▌&amp;quot;)

        # Final display without cursor
        if accumulated_thinking:
            with thinking_display.container():
                with st.expander(&amp;quot;🧠 View Thinking Process&amp;quot;, expanded=False):
                    st.markdown(convert_latex_delimiters(accumulated_thinking))
        answer_display.markdown(convert_latex_delimiters(accumulated_answer))

        # Save to chat history
        st.session_state.messages.append({
            &amp;quot;role&amp;quot;: &amp;quot;assistant&amp;quot;,
            &amp;quot;content&amp;quot;: accumulated_answer,
            &amp;quot;thinking&amp;quot;: accumulated_thinking or None
        })
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;Breaking Down the Code&lt;/h2&gt;
&lt;h3&gt;Page Configuration&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;st.set_page_config(page_title=&amp;quot;DeepSeek R1 Chat&amp;quot;, page_icon=&amp;quot;🤖&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This sets up our Streamlit page with a custom title and icon that appears in the browser tab.&lt;/p&gt;
&lt;h3&gt;LaTeX Conversion Function&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def convert_latex_delimiters(text):
    text = text.replace(r'\[', '$$')
    text = text.replace(r'\]', '$$')
    text = text.replace(r'\(', '$')
    text = text.replace(r'\)', '$')
    return text
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;DeepSeek R1 often outputs mathematical notation using LaTeX syntax. However, it uses &lt;code&gt;\[&lt;/code&gt; and &lt;code&gt;\]&lt;/code&gt; for display math and &lt;code&gt;\(&lt;/code&gt; and &lt;code&gt;\)&lt;/code&gt; for inline math. Streamlit's markdown renderer expects &lt;code&gt;$$&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; instead, so this function converts between the two formats.&lt;/p&gt;
&lt;h3&gt;Initializing Chat History&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;if &amp;quot;messages&amp;quot; not in st.session_state:
    st.session_state.messages = []
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Streamlit reruns your entire script on every interaction. To preserve chat history between reruns, we store messages in &lt;code&gt;st.session_state&lt;/code&gt;, which persists across reruns.&lt;/p&gt;
&lt;h3&gt;Displaying Chat History&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;for msg in st.session_state.messages:
    with st.chat_message(msg[&amp;quot;role&amp;quot;]):
        if msg[&amp;quot;role&amp;quot;] == &amp;quot;assistant&amp;quot; and msg.get(&amp;quot;thinking&amp;quot;):
            with st.expander(&amp;quot;🧠 View Thinking Process&amp;quot;, expanded=False):
                st.markdown(convert_latex_delimiters(msg[&amp;quot;thinking&amp;quot;]))
        st.markdown(convert_latex_delimiters(msg[&amp;quot;content&amp;quot;]))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This loop displays all previous messages. For assistant messages, if there's thinking content, we show it in a collapsible expander. This keeps the interface clean while still making the reasoning process available.&lt;/p&gt;
&lt;h3&gt;Handling User Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;user_input = st.chat_input(&amp;quot;Ask DeepSeek R1&amp;quot;)

if user_input:
    st.session_state.messages.append({&amp;quot;role&amp;quot;: &amp;quot;user&amp;quot;, &amp;quot;content&amp;quot;: user_input})
    with st.chat_message(&amp;quot;user&amp;quot;):
        st.markdown(user_input)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;When a user types a message, we add it to our message history and display it immediately in the chat interface.&lt;/p&gt;
&lt;h3&gt;Streaming the Response&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;with st.spinner(&amp;quot;Thinking...&amp;quot;, show_time=True):
    stream = ollama.chat(
        model=&amp;quot;deepseek-r1:1.5b&amp;quot;,
        messages=[{&amp;quot;role&amp;quot;: m[&amp;quot;role&amp;quot;], &amp;quot;content&amp;quot;: m[&amp;quot;content&amp;quot;]}
                  for m in st.session_state.messages],
        stream=True,
        think=True,
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The critical part here is &lt;code&gt;think=True&lt;/code&gt;. This tells Ollama to return DeepSeek R1's reasoning process separately from its final answer. The &lt;code&gt;stream=True&lt;/code&gt; parameter makes the response appear word-by-word rather than all at once.&lt;/p&gt;
&lt;h3&gt;Processing the Stream&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;for chunk in stream:
    chunk_msg = chunk.get(&amp;quot;message&amp;quot;, {})

    # Stream thinking content
    if chunk_msg.get(&amp;quot;thinking&amp;quot;):
        accumulated_thinking += chunk_msg[&amp;quot;thinking&amp;quot;]
        with thinking_display.container():
            with st.expander(&amp;quot;🧠 View Thinking Process&amp;quot;, expanded=True):
                st.markdown(convert_latex_delimiters(accumulated_thinking) + &amp;quot;▌&amp;quot;)

    # Stream answer content
    if chunk_msg.get(&amp;quot;content&amp;quot;):
        accumulated_answer += chunk_msg[&amp;quot;content&amp;quot;]
        answer_display.markdown(convert_latex_delimiters(accumulated_answer) + &amp;quot;▌&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As chunks arrive from Ollama, they contain either &amp;quot;thinking&amp;quot; or &amp;quot;content&amp;quot; fields. We accumulate both separately and display them in real-time. The &amp;quot;▌&amp;quot; character creates a blinking cursor effect showing that content is still streaming.&lt;/p&gt;
&lt;h3&gt;Finalizing the Display&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;if accumulated_thinking:
    with thinking_display.container():
        with st.expander(&amp;quot;🧠 View Thinking Process&amp;quot;, expanded=False):
            st.markdown(convert_latex_delimiters(accumulated_thinking))
answer_display.markdown(convert_latex_delimiters(accumulated_answer))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Once streaming completes, we remove the cursor and collapse the thinking expander by default. This gives users a clean final view with the option to expand and see the reasoning.&lt;/p&gt;
&lt;h3&gt;Saving to History&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;st.session_state.messages.append({
    &amp;quot;role&amp;quot;: &amp;quot;assistant&amp;quot;,
    &amp;quot;content&amp;quot;: accumulated_answer,
    &amp;quot;thinking&amp;quot;: accumulated_thinking or None
})
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Finally, we save the complete response to our message history so it persists when the page reruns.&lt;/p&gt;
&lt;h2&gt;Running the Application&lt;/h2&gt;
&lt;p&gt;Save the code as &lt;code&gt;streamlit_example.py&lt;/code&gt; and run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;streamlit run streamlit_example.py
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Your browser will open with the chatbot interface. Try asking it a math question like &amp;quot;What is 15 times 23?&amp;quot; and you'll see it show its reasoning process before giving the final answer.&lt;/p&gt;
&lt;p&gt;Here was my result:
&lt;img src="https://www.mindfiretechnology.com/blog/media/screenshot-2025-12-04-141355.png" alt="Deepseek R1 Running in Streamlit" /&gt;&lt;/p&gt;
&lt;p&gt;One annoying aspect of Deepseek R1 is that I've seen it got stuck in a 'thinking loop'. I once asked it to give me the first 12 digits of PI and it couldn't decide if that included the '3.' or not. It kept going back and forth trying to decide for 10 minutes before I gave up. (Rather than just giving me both answers.) &lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/screenshot-2025-12-04-132204.png" alt="enter image description here" /&gt;&lt;/p&gt;
&lt;h2&gt;Key Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Runs completely locally with no API calls&lt;/li&gt;
&lt;li&gt;Shows the model's reasoning process in expandable sections&lt;/li&gt;
&lt;li&gt;Streams responses in real-time for a better user experience&lt;/li&gt;
&lt;li&gt;Properly renders mathematical notation&lt;/li&gt;
&lt;li&gt;Maintains conversation history&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;With just 90 lines of Python, we've created a functional chatbot interface for DeepSeek R1 that showcases one of its most interesting features: transparent reasoning. The combination of Streamlit's simple API and Ollama's local model hosting makes it easy to experiment with AI models without worrying about API costs or privacy concerns.&lt;/p&gt;
&lt;p&gt;The complete source code is available above. Feel free to modify and extend it for your own projects!&lt;/p&gt;
</description>
      <pubDate>Tue, 30 Dec 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-12-30T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2683</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/how-dspy-optimizes-prompts/</link>
      <category>System.String[]</category>
      <title>How DSPy Optimizes Prompts</title>
      <description>&lt;p&gt;In our previous posts (&lt;a href="https://www.mindfiretechnology.com/blog/archive/dspy-prompt-your-llm-like-its-code/"&gt;here&lt;/a&gt;, &lt;a href="https://www.mindfiretechnology.com/blog/archive/dspy-a-powerful-but-sometimes-dangerous-prompting-tool/"&gt;here&lt;/a&gt;, and &lt;a href="https://www.mindfiretechnology.com/blog/archive/dspy-how-it-works/"&gt;here&lt;/a&gt;), we looked at &lt;a href="https://dspy.ai/"&gt;DSPy&lt;/a&gt; and how it turns Large Language Models (LLMs) into structured, type-safe Python functions. You define a class with input and output fields, and DSPy builds the prompt for you automatically.  
&lt;/p&gt;
&lt;p&gt;But here’s the thing — sometimes your initial prompt isn’t perfect. The model might get the instructions a little wrong, or your few-shot examples might not be ideal. That’s where DSPy optimizers like &lt;strong&gt;MIPROv2&lt;/strong&gt; comes in. It’s a tool in DSPy that automatically tweaks your prompts and few-shot examples to make the model behave better. You can &lt;a href="https://dspy.ai/api/optimizers/MIPROv2/"&gt;read more about it on the here&lt;/a&gt;.  
&lt;/p&gt;
&lt;p&gt;In this post, we’ll show you how DSPy can &lt;strong&gt;optimize prompts&lt;/strong&gt; using MIPROv2. We’ll go from an instruction that gives poor results to one that gives consistently correct results — and we’ll show you exactly what changes along the way. You’ll see how the model’s instructions and examples evolve, and why this makes your LLM programs more reliable.&lt;/p&gt;
&lt;p&gt;You can find &lt;a href="https://github.com/brucenielson/DSPyTutorials/blob/403b2f4a11b0c03aec5b684dcd60984887f8b737/dspy_optimizing.py"&gt;the full code for this blog post on my github repo here.&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;A Poorly Defined Prompt&lt;/h2&gt;
&lt;p&gt;The fact is that DSPy is so good at building prompts that I actually struggled to come up with a good simple example of how it can automatically improve prompts for you. It often got 100% accuracy on sentiment analysis right out of the box. So to give it a real challenge I rewrote my 'Classify' function like this. Originally we had:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Classify(dspy.Signature):
    &amp;quot;&amp;quot;&amp;quot;Classify sentiment of a given sentence.&amp;quot;&amp;quot;&amp;quot;
    sentence: str = dspy.InputField()
    sentiment: Literal[&amp;quot;positive&amp;quot;, &amp;quot;negative&amp;quot;, &amp;quot;neutral&amp;quot;] = dspy.OutputField()
    confidence: float = dspy.OutputField()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And now we have the much vaguer: &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class AnalyzeText(dspy.Signature):
    &amp;quot;&amp;quot;&amp;quot;Process the input.&amp;quot;&amp;quot;&amp;quot;  # ← Useless instruction
    text: str = dspy.InputField(desc=&amp;quot;input data&amp;quot;)
    label: Literal[&amp;quot;Bingo!&amp;quot;, &amp;quot;Hmmm...&amp;quot;, &amp;quot;Oink!&amp;quot;] = dspy.OutputField(desc=&amp;quot;output&amp;quot;)  # Shuffled order!
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Instead of being called &amp;quot;Classify&amp;quot; (which is a dead give away we're doing sentiment analysis) we now call it &amp;quot;AnalyzeText&amp;quot;. And &amp;quot;sentence&amp;quot; and &amp;quot;sentiment&amp;quot; are now &amp;quot;text&amp;quot; and &amp;quot;label&amp;quot;. Plus the labels are 'Bingo!', 'Oink!' and 'Hmmm...' &lt;/p&gt;
&lt;p&gt;The first time I tried this, Gemini figured out on its own that 'Bingo!' was positive, 'Oink!' was negative, and 'Hmmm...' was neutral. So I had to swap orders around. And now 'Bingo!' is neutral, 'Oink!' is positive, and 'Hmmm...' is negative. So now there is no way Gemini can figure out what the correct labels are!&lt;/p&gt;
&lt;p&gt;And sure enough, it does a terrible job labeling the data out of the box:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;============================================================
BEFORE OPTIMIZATION
============================================================

📋 Initial Instruction:
&amp;quot;Process the input.&amp;quot;

🎭 INVERTED Nonsense Categories (counterintuitive!):
   'Oink!'   = positive sentiment (opposite of what you'd expect!)
   'Bingo!'  = neutral/mixed sentiment (not positive!)
   'Hmmm...' = negative sentiment (not thoughtful!)

The model will naturally guess WRONG without training!

🧪 Testing on dev set (without optimization):
  ✗ 'The first half was amazing but then it fell a...' → Hmmm...  (expected: Bingo!)
  ✗ 'Not terrible but nothing special....' → Hmmm...  (expected: Bingo!)
  ✗ 'An absolute masterpiece in every way!...' → Bingo!   (expected: Oink!)
  ✓ 'I wanted to like it but it was just awful....' → Hmmm...  (expected: Hmmm...)
  ✗ 'Has its moments but overall just average....' → Hmmm...  (expected: Bingo!)

📊 Initial accuracy: 1/5 (20%)
    ↑ Should be near random chance (~33%) with no instruction!
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I'm sort of surprised it even got 20% right. &lt;/p&gt;
&lt;h2&gt;Running the Optimizer&lt;/h2&gt;
&lt;p&gt;Now that we’ve seen how DSPy builds prompts automatically, let’s see how it can improve them using the MIPROv2 optimizer.&lt;/p&gt;
&lt;p&gt;We’ll take our deliberately confusing example above to make the optimizer necessary. We also define a small &lt;strong&gt;training set&lt;/strong&gt; and &lt;strong&gt;validation set&lt;/strong&gt; with tricky examples to test the model. The metric simply checks whether the model predicts the expected label:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def metric(example, pred, trace=None):
    return example.label == pred.label
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;As we saw, this initially only gets 20% correct. The full initial prompt can also be inspected using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lm.inspect_history(n=1)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This shows exactly how DSPy was instructing the LLM before optimization. Here is the prompt that is built initially:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;System message:

Your input fields are:
1. `text` (str): input data
Your output fields are:
1. `label` (Literal['Bingo!', 'Hmmm...', 'Oink!']): output
All interactions will be structured in the following way, with the appropriate values filled in.

[[ ## text ## ]]
{text}

[[ ## label ## ]]
{label}        # note: the value you produce must exactly match (no extra characters) one of: Bingo!; Hmmm...; Oink!

[[ ## completed ## ]]
In adhering to this structure, your objective is:
        Process the input.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Let's run the MIPROv2 optimizer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;optimizer = MIPROv2(metric=metric, auto=&amp;quot;light&amp;quot;)
optimized = optimizer.compile(
    student=program,
    trainset=trainset,
    valset=devset,
)

...

if hasattr(optimized, 'predictor'):
    optimized_instruction = optimized.predictor.signature.__doc__
    print(f'&amp;quot;{optimized_instruction}&amp;quot;')

    original_instruction = program.predictor.signature.__doc__
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;After the MIPROv2 optimizer runs we get much better results:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;============================================================
AFTER OPTIMIZATION
============================================================

📋 Optimized Instruction:
&amp;quot;You are a sentiment analysis model. Classify the given text into one of the following categories: &amp;quot;Oink!&amp;quot; for strongly positive, &amp;quot;Hmmm...&amp;quot; for negative, and &amp;quot;Bingo!&amp;quot; for mixed or neutral sentiments.&amp;quot;

✨ INSTRUCTION CHANGED! ✨

  Before: 'Process the input.'

  After:  'You are a sentiment analysis model. Classify the given text into one of the following categories: &amp;quot;Oink!&amp;quot; for strongly positive, &amp;quot;Hmmm...&amp;quot; for negative, and &amp;quot;Bingo!&amp;quot; for mixed or neutral sentiments.'

  The optimizer learned the nonsense mapping!

🧪 Testing optimized version on dev set:
  ✓ 'The first half was amazing but then it fell a...' → Bingo!   (expected: Bingo!)
  ✓ 'Not terrible but nothing special....' → Bingo!   (expected: Bingo!)
  ✓ 'An absolute masterpiece in every way!...' → Oink!    (expected: Oink!)
  ✓ 'I wanted to like it but it was just awful....' → Hmmm...  (expected: Hmmm...)
  ✓ 'Has its moments but overall just average....' → Bingo!   (expected: Bingo!)

📊 Optimized accuracy: 5/5 (100%)
    ↑ Should be much better now!
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Wow! 100% success now despite the misleading labels I'm expecting! Okay, how does it do it? What does the final prompt now look like?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;System message:

Your input fields are:
1. `text` (str): input data
Your output fields are:
1. `label` (Literal['Bingo!', 'Hmmm...', 'Oink!']): output
All interactions will be structured in the following way, with the appropriate values filled in.

[[ ## text ## ]]
{text}

[[ ## label ## ]]
{label}        # note: the value you produce must exactly match (no extra characters) one of: Bingo!; Hmmm...; Oink!

[[ ## completed ## ]]
In adhering to this structure, your objective is: 
        You are a sentiment analysis model. Classify the given text into one of the following categories: &amp;quot;Oink!&amp;quot; for strongly positive, &amp;quot;Hmmm...&amp;quot; for negative, and &amp;quot;Bingo!&amp;quot; for mixed or neutral sentiments.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Whoa! It changed the instructions from &amp;quot;Process the input.&amp;quot; (My default) to &amp;quot;You are a sentiment analysis model. Classify the given text into one of the following categories: 'Oink!' for strongly positive, 'Hmmm...' for negative, and 'Bingo!' for mixed or neutral sentiments.&amp;quot;&lt;/p&gt;
&lt;p&gt;No wonder it's now getting 100% correct! &lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;What this example shows is the real power of DSPy’s optimizer workflow. With just a few lines of Python, we went from a deliberately confusing instruction that produced almost random results to a fully optimized prompt that got 100% accuracy on our dev set.&lt;/p&gt;
&lt;p&gt;The key takeaway is that DSPy doesn’t just wrap an LLM in a function — it gives you control over how the model interprets your instructions and examples, and it can automatically improve them. That means:&lt;/p&gt;
&lt;p&gt;You can use arbitrary input/output field names and even nonsense labels, and the optimizer will learn the correct mapping.&lt;/p&gt;
&lt;p&gt;You can systematically improve your prompts without manually guessing how to rewrite them using empirical evidence from a test set.&lt;/p&gt;
&lt;p&gt;The process is reproducible and type-safe, so you can confidently switch models or update instructions without breaking your code. If you do switch models, just re-run your optimizer and it will build prompts appropriate for the new model!&lt;/p&gt;
&lt;p&gt;In short, DSPy + MIPROv2 turns what used to be trial-and-error prompt engineering into a structured, programmatic, and testable process. For anyone building reliable LLM-powered applications, this is a huge productivity and quality-of-results win.&lt;/p&gt;
</description>
      <pubDate>Tue, 23 Dec 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-12-23T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2682</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/dspy-how-it-works/</link>
      <category>System.String[]</category>
      <title>How DSPy Builds Prompts</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/dspy-prompt-your-llm-like-its-code/"&gt;In previous posts&lt;/a&gt;, we talked about what &lt;a href="https://dspy.ai/"&gt;DSPy&lt;/a&gt;, a tool you can use to treat Large Language Model (LLM) interactions as functions written in code rather than via prompting. &lt;a href="https://www.mindfiretechnology.com/blog/archive/dspy-a-powerful-but-sometimes-dangerous-prompting-tool/"&gt;In a follow-on post&lt;/a&gt;, we saw examples of how to use it out of the box. &lt;/p&gt;
&lt;p&gt;Okay, that is all nice and good, but how exactly does it work? I mean ultimately you interact with an LLM via prompts, right? So what is all this code doing?&lt;/p&gt;
&lt;h2&gt;How Classify Works&lt;/h2&gt;
&lt;p&gt;In this post, we're going to answer that question. You can &lt;a href="https://github.com/brucenielson/DSPyTutorials/blob/994f1720dbd775ba91bbf26aa36eb1833ba0bb20/dspy_prompt_building.py"&gt;find my code for this post in my github repo&lt;/a&gt;. Let's start with the same Classify signature as from our previous post:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Classify(dspy.Signature):
    &amp;quot;&amp;quot;&amp;quot;Classify sentiment of a given sentence.&amp;quot;&amp;quot;&amp;quot;
    sentence: str = dspy.InputField()
    sentiment: Literal[&amp;quot;positive&amp;quot;, &amp;quot;negative&amp;quot;, &amp;quot;neutral&amp;quot;] = dspy.OutputField()
    confidence: float = dspy.OutputField()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We then turn this into a real Classify function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model = setup_model()
classify = dspy.Predict(Classify)

result = classify(
    sentence=&amp;quot;This book was super fun to read, though not the last chapter.&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Recall that the LLM produces a structured output like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Prediction(
    sentiment='neutral',
    confidence=0.8
)
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;p&gt;But how does it do it? Obviously it is building a prompt behind the scenes. Can we see what it is doing? Yes, by inspecting the history:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;print(&amp;quot;\n--- Prompt Built by DSPy ---&amp;quot;)
print(model.inspect_history(n=1))
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.inspect_history() is a method that keeps a record of every request DSPy has sent to the model in order.&lt;/li&gt;
&lt;li&gt;The parameter n=1 means “show me the last 1 interaction.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is the (somewhat long) result:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Classification Result:
Prediction(
    sentiment='neutral',
    confidence=0.8
)

--- Prompt Built by DSPy ---

[2025-11-20T10:39:12.956671]

System message:

Your input fields are:
1. `sentence` (str):
Your output fields are:
1. `sentiment` (Literal['positive', 'negative', 'neutral']): 
2. `confidence` (float):
All interactions will be structured in the following way, with the appropriate values filled in.

[[ ## sentence ## ]]
{sentence}

[[ ## sentiment ## ]]
{sentiment}        # note: the value you produce must exactly match (no extra characters) one of: positive; negative; neutral

[[ ## confidence ## ]]
{confidence}        # note: the value you produce must be a single float value

[[ ## completed ## ]]
In adhering to this structure, your objective is: 
        Classify sentiment of a given sentence.

User message:

[[ ## sentence ## ]]
This book was super fun to read, though not the last chapter.

Respond with the corresponding output fields, starting with the field `[[ ## sentiment ## ]]` (must be formatted as a valid Python Literal['positive', 'negative', 'neutral']), then `[[ ## confidence ## ]]` (must be formatted as a valid Python float), and then ending with the marker for `[[ ## completed ## ]]`.

Response:

[[ ## sentiment ## ]]
neutral

[[ ## confidence ## ]]
0.8

[[ ## completed ## ]]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;What we see here illustrates how DSPy builds structured prompts:&lt;/p&gt;
&lt;p&gt;System message: DSPy tells the model exactly what the input and output fields are, and how the response should be formatted.&lt;/p&gt;
&lt;p&gt;Field markers ([[ ## sentence ## ]], etc.): These placeholders map your Python class fields to text in the prompt. The LLM fills them in with actual values (sentence input, sentiment and confidence output).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[ ## sentence ## ]]
{sentence}

[[ ## sentiment ## ]]
{sentiment}        # note: the value you produce must exactly match (no extra characters) one of: positive; negative; neutral
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Docstring usage: The docstring from Classify (“Classify sentiment of a given sentence.”) becomes part of the instructions, guiding the model on the task.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[ ## completed ## ]]
In adhering to this structure, your objective is: 
        Classify sentiment of a given sentence.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This structure ensures the LLM returns a predictable, typed response — which is exactly why we get Prediction(sentiment='neutral', confidence=0.8) instead of an unstructured string.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this post, we saw how DSPy automatically converts a Python class into a fully structured prompt for Gemini. By inspecting the history, we can see exactly how the class name, fields, and docstring are used to generate instructions that guide the model’s response.&lt;/p&gt;
&lt;p&gt;This approach gives you predictable, type-safe outputs without manually writing prompts. You can experiment by tweaking input/output fields or the docstring and immediately see how the prompt — and the model’s behavior — changes.&lt;/p&gt;
&lt;p&gt;Ultimately, DSPy lets you treat LLMs like functions in your code, while still giving full transparency into the underlying prompts. It’s a powerful way to combine the flexibility of LLMs with the reliability of typed, structured programming.&lt;/p&gt;
</description>
      <pubDate>Tue, 16 Dec 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-12-16T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2658</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/dspy-a-powerful-but-sometimes-dangerous-prompting-tool/</link>
      <category>System.String[]</category>
      <title>DSPy: A Powerful (But Sometimes Dangerous) Prompting Tool</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/dspy-prompt-your-llm-like-its-code/"&gt;In our last post&lt;/a&gt;, we introduced &lt;a href="https://dspy.ai/"&gt;DSPy&lt;/a&gt;, a tool to treat prompt building for your Large Language Model (LLM) like it is a Python function. This time we're going to use another modified example off &lt;a href="https://dspy.ai/"&gt;their 'getting started' page&lt;/a&gt; and then play with it a little and get a feel for how DSPy works.&lt;/p&gt;
&lt;h2&gt;Writing a &amp;quot;Classify&amp;quot; Function&lt;/h2&gt;
&lt;p&gt;Let's use DSPy to write a sentiment classification function. Here is the code (&lt;a href="https://github.com/brucenielson/DSPyTutorials/tree/ac7077d57071135965a1b88710405832ed07ef90"&gt;found in this github repo&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Classify(dspy.Signature):
    &amp;quot;&amp;quot;&amp;quot;Classify sentiment of a given sentence.&amp;quot;&amp;quot;&amp;quot;
    sentence: str = dspy.InputField()
    sentiment: Literal[&amp;quot;positive&amp;quot;, &amp;quot;negative&amp;quot;, &amp;quot;neutral&amp;quot;] = dspy.OutputField()
    confidence: float = dspy.OutputField()
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Explanation of the &lt;code&gt;Classify&lt;/code&gt; function, line by line:&lt;/h3&gt;
&lt;p&gt;Let's analyze what is going on here. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;class Classify(dspy.Signature):&lt;/code&gt;&lt;br /&gt;
  This defines a new DSPy &lt;strong&gt;Signature&lt;/strong&gt; class named &lt;code&gt;Classify&lt;/code&gt;. By subclassing &lt;code&gt;dspy.Signature&lt;/code&gt;, you declare that this class specifies the input/output contract for a DSPy module. (&lt;a href="https://dspy.ai/learn/programming/signatures/"&gt;DSPy signatures docs&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;quot;&amp;quot;&amp;quot;Classify sentiment of a given sentence.&amp;quot;&amp;quot;&amp;quot;&lt;/code&gt;&lt;br /&gt;
  A docstring that gives a human-readable description of the task: sentiment classification of a sentence.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sentence: str = dspy.InputField()&lt;/code&gt;&lt;br /&gt;
  Declares an input field named &lt;code&gt;sentence&lt;/code&gt; of type &lt;code&gt;str&lt;/code&gt;. This tells the module it will receive a sentence as input.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;sentiment: Literal[&amp;quot;positive&amp;quot;, &amp;quot;negative&amp;quot;, &amp;quot;neutral&amp;quot;] = dspy.OutputField()&lt;/code&gt;&lt;br /&gt;
  Declares an output field called &lt;code&gt;sentiment&lt;/code&gt; whose type is a &lt;code&gt;Literal&lt;/code&gt; limited to the values &lt;code&gt;&amp;quot;positive&amp;quot;&lt;/code&gt;, &lt;code&gt;&amp;quot;negative&amp;quot;&lt;/code&gt;, or &lt;code&gt;&amp;quot;neutral&amp;quot;&lt;/code&gt;. This instructs DSPy to parse and return one of those labels.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;confidence: float = dspy.OutputField()&lt;/code&gt;&lt;br /&gt;
  Declares another output field named &lt;code&gt;confidence&lt;/code&gt; of type &lt;code&gt;float&lt;/code&gt;. This tells DSPy to return a numeric confidence score alongside the label.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What this signature does overall:&lt;/strong&gt;&lt;br /&gt;
Together, the fields form a function-like contract: given a &lt;code&gt;sentence&lt;/code&gt; string, the module will return a &lt;code&gt;sentiment&lt;/code&gt; label and a &lt;code&gt;confidence&lt;/code&gt; float. DSPy uses this signature to (1) build the underlying prompt, (2) call the LLM, and (3) parse &amp;amp; cast the model output into the declared typed fields — returning a structured result instead of a raw string.&lt;/p&gt;
&lt;h3&gt;Running the Code&lt;/h3&gt;
&lt;p&gt;Now we'll use this function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if __name__ == &amp;quot;__main__&amp;quot;:
    set_model()
    dspy.configure()

    classify = dspy.Predict(Classify)
    result = classify(sentence=&amp;quot;This book was super fun to read, though not the last chapter.&amp;quot;)
    print(&amp;quot;\nClassification Example:&amp;quot;)
    print(result)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;We first use the DSPy Predict module and pass in our Classify class to it and we get back a new classify function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;classify = dspy.Predict(Classify)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now we can use the classify method by calling it and passing in a &lt;code&gt;sentence&lt;/code&gt; parameter, just like we specified in our class.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;result = classify(sentence=&amp;quot;This book was super fun to read, though not the last chapter.&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The sentence we're trying to classify is &amp;quot;This book was super fun to read, though not the last chapter.&amp;quot; This is positive for the first half and negative for the second half. So, unsurprisingly, when I run this code I get back a result like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Classification Example:
Prediction(
    sentiment='neutral',
    confidence=0.8
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Gemini is 80% confident that this is a neutral sentence. &lt;/p&gt;
&lt;p&gt;However, let's play around with this a bit and we'll see the dangers of not fully controlling your prompts. Let's intentionally try to screw things up a bit by redoing &lt;code&gt;Classify&lt;/code&gt; like this:&lt;/p&gt;
&lt;p&gt;class Classify(dspy.Signature):
    &amp;quot;&amp;quot;&amp;quot;Classify sentiment of a given sentence.&amp;quot;&amp;quot;&amp;quot;
    sentence: str = dspy.InputField()
    sentiment: Literal[&amp;quot;positive&amp;quot;, &amp;quot;negative&amp;quot;, &amp;quot;neutral&amp;quot;, &amp;quot;boo!&amp;quot;] = dspy.OutputField()
    confidence: float = dspy.OutputField()&lt;/p&gt;
&lt;p&gt;I added &amp;quot;boo!&amp;quot; as a possible sentiment, which makes no real sense. Now run the code again and we get:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Classification Example:
Prediction(
    sentiment='positive',
    confidence=0.75
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By adding &amp;quot;boo!&amp;quot; as an option, we went from 80% confident this was a neutral statement to 75% confident it is positive. Why? I have no idea and I doubt the LLM does either.&lt;/p&gt;
&lt;h2&gt;How Prompts Are Built&lt;/h2&gt;
&lt;p&gt;You might, at this point, be wondering how this function is turned into a prompt that is sent to Gemini. The answer is that it uses the name of the class, the class properties, and even the docstring to come up with a prompt. To prove this, let's change the &lt;code&gt;Classify&lt;/code&gt; method to instead be:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Classify(dspy.Signature):
    &amp;quot;&amp;quot;&amp;quot;Return &amp;quot;boo!&amp;quot; as sentiment every time&amp;quot;&amp;quot;&amp;quot;
    sentence: str = dspy.InputField()
    sentiment: Literal[&amp;quot;positive&amp;quot;, &amp;quot;negative&amp;quot;, &amp;quot;neutral&amp;quot;, &amp;quot;boo!&amp;quot;] = dspy.OutputField()
    confidence: float = dspy.OutputField()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice that all I did was change the docstring to tell it to return &amp;quot;boo!&amp;quot; every time. And we now get:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Classification Example:
Prediction(
    sentiment='boo!',
    confidence=1.0
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Nice, eh? The docstring isn't just a comment any more, it's part of how you code the function! &lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Hopefully this short demo will help you understand how DSPy turns its classes/functions into prompts. There is clearly a lot of power here, but also some danger. The best reason to do this is if you plan to use DSPy's optimizers to let your software come up with the testably best prompts. Imagine how you might simply unplug one model, plug in another, then rerun the optimizer. You could easily move from one LLM to another using the same code base!&lt;/p&gt;
</description>
      <pubDate>Tue, 09 Dec 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-12-09T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2657</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/dspy-prompt-your-llm-like-its-code/</link>
      <category>System.String[]</category>
      <title>DSPy: Prompt your LLM Like It's Code</title>
      <description>&lt;p&gt;&lt;a href="https://dspy.ai/"&gt;DSPy (pronounced &amp;quot;dee-s-pie&amp;quot;) is an open-source Python framework&lt;/a&gt; that lets you exchange prompt building for code. Sounds impossible, right? What it really does is cleverly turn Python code into prompts behind the scenes. You get to specify what you want using code. For example, you might define a &lt;code&gt;Classify&lt;/code&gt; function with specific inputs and expected outputs. Behind the scenes, DSPy converts that into a prompt, sends it to your Large Language Model (LLM), and returns a result formatted according to your specifications. The end result feels like you wrote code instead of building natural language prompts.&lt;/p&gt;
&lt;h2&gt;Why DSPy Instead of Prompt Building?&lt;/h2&gt;
&lt;p&gt;Why might you want to do this? Well, first of all, maybe you don’t. I often like to have full control over everything, so when I don’t know what’s being sent to the LLM, I’m like a nervous public speaker in front of a large auditorium—without being told what my topic is.&lt;/p&gt;
&lt;p&gt;However, think about this differently. Using DSPy forces you to treat your interactions with the LLM as if they were functions with defined inputs and outputs. It formalizes these interactions into Python functions that are independent of the underlying LLM. That alone makes it valuable—but the real power comes when you use DSPy to optimize those interactions. &lt;/p&gt;
&lt;p&gt;Prompt optimization in DSPy works by running your program on a set of developer-provided training examples and collecting traces of input/output behavior. It then systematically &lt;strong&gt;proposes and evaluates new prompt instructions&lt;/strong&gt; (and optionally fine-tunes the model itself), as well as few-shot examples, to maximize your chosen metric. Yes, you heard that right! Instead of writing each prompt and relying on your gut (or ad hoc testing), DSPy will take training examples and try out different prompts until it finds the ones that perform best.&lt;/p&gt;
&lt;p&gt;Even if you don’t use DSPy’s optimizers, there’s still value in the framework thanks to its pre-built modules. In this post, I’ll show you how to build a basic chain-of-thought model &lt;strong&gt;without having to write your own chain-of-thought prompts&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Getting Started with DSPy&lt;/h2&gt;
&lt;p&gt;First install DSPy:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install -U dspy
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now here is how to use the DSPy Chain of Thought module. &amp;quot;Chain of thought&amp;quot; is a prompting technique for an LLM that tells it to think through the problem step-by-step and reason when giving an answer. It was invented by Google in a famous 2022 paper called &amp;quot;&lt;a href="https://arxiv.org/abs/2201.11903"&gt;Chain-of-Thought Prompting Elicits Reasoning in Large Language Models&lt;/a&gt;&amp;quot;. It allows an LLM to reason better. This example is a modified version of the &lt;a href="https://dspy.ai/"&gt;the 'getting started' tutorial from the DSPy home page&lt;/a&gt;. I have modified it to use Google's Gemini as a model, which was basically just plug and play. I also added my standard get_secret function, &lt;a href="https://github.com/brucenielson/DSPyTutorials/blob/ac7077d57071135965a1b88710405832ed07ef90/general_utils.py"&gt;as found here&lt;/a&gt;. It allows you to load a password out of a file of your choosing to avoid accidentally checking in an API key or password into your code base.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import dspy
from general_utils import get_secret

def set_model():
    # Load Gemini secret and configure LM
    gemini_secret: str = get_secret(r'D:\Documents\Secrets\gemini_secret.txt')
    lm = dspy.LM(&amp;quot;gemini/gemini-2.5-flash&amp;quot;, api_key=gemini_secret)
    dspy.configure(lm=lm)
    return lm

def chain_of_thought():
    math = dspy.ChainOfThought(&amp;quot;question -&amp;gt; answer: float&amp;quot;)
    result = math(question=&amp;quot;Two dice are tossed. What is the probability that the sum equals two?&amp;quot;)
    print(result)

if __name__ == &amp;quot;__main__&amp;quot;:
    set_model()

    print(&amp;quot;Chain of Thought Example:&amp;quot;)
    chain_of_thought()
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can find &lt;a href="https://github.com/brucenielson/DSPyTutorials/tree/ac7077d57071135965a1b88710405832ed07ef90"&gt;my full code here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Understanding the ChainOfThought Signature&lt;/h2&gt;
&lt;p&gt;Here we're using the ChainOfThought module:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;math = dspy.ChainOfThought(&amp;quot;question -&amp;gt; answer: float&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This signature string (&lt;code&gt;&amp;quot;question -&amp;gt; answer: float&amp;quot;&lt;/code&gt;) is how DSPy describes the &lt;strong&gt;inputs&lt;/strong&gt; and &lt;strong&gt;outputs&lt;/strong&gt; of a module in a compact, human-readable way.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The part before the arrow (&lt;code&gt;question&lt;/code&gt;) names the input parameter the module expects.&lt;/li&gt;
&lt;li&gt;The part after the arrow (&lt;code&gt;answer: float&lt;/code&gt;) names the output and declares its type (&lt;code&gt;float&lt;/code&gt; in this case).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why this matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prompt generation&lt;/strong&gt;: DSPy uses that signature to automatically build the underlying prompt. Instead of you writing the prompt text,
  DSPy knows you expect a &lt;code&gt;question&lt;/code&gt; and that the final &lt;code&gt;answer&lt;/code&gt; should
  be a &lt;code&gt;float&lt;/code&gt;, so it steers the model to produce a suitably formatted
  response.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Parsing &amp;amp; validation&lt;/strong&gt;: After the LLM replies, DSPy parses and casts the model output into the declared type. In the example above, the returned &lt;code&gt;answer&lt;/code&gt; is converted to a Python &lt;code&gt;float&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structured traces&lt;/strong&gt;: For chain-of-thought modules in particular, DSPy usually returns both a &lt;code&gt;reasoning&lt;/code&gt; trace (the step-by-step explanation the model produced) and the typed &lt;code&gt;answer&lt;/code&gt;. That’s why your output looked like:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Chain of Thought Example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Prediction(
        reasoning='When two dice are tossed, each die can show a number from 1 to 6.\nThe total number of possible outcomes is 6 (for the first die) * 6 (for the second die) = 36.\nTo find the sum that equals two, we need to list the combinations:\nThe only combination that results in a sum of two is (1, 1).\nThere is only 1 favorable outcome.\nThe probability is the number of favorable outcomes divided by the total number of possible outcomes.\nProbability = 1/36.\n\nTo express this as a float: 1 / 36 = 0.027777777777777776\nRounding to a reasonable number of decimal places, or just providing the direct float value.',
        answer=0.027777777777777776
)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notice that the reasoning trace is how the LLM reasoned (via the chain-of-thought technique) to come up with an answer.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;We have introduced DSPy and explained why you might want to treat your prompt building as code. We've installed it and used its out-of-the-box chain-of-thought module to show what you can do with minimal programming. Next time we'll look deeper at how DSPy works.&lt;/p&gt;
</description>
      <pubDate>Tue, 02 Dec 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-12-02T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2684</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/google-gemini-3-makes-a-huge-leap-on-the-arc-agi-benchmark/</link>
      <category>System.String[]</category>
      <title>Google Gemini 3 Makes a Huge Leap on the ARC‑AGI Benchmark</title>
      <description>&lt;p&gt;Google’s &lt;strong&gt;Gemini 3&lt;/strong&gt; has posted a standout result on the &lt;a href="https://arcprize.org/leaderboard"&gt;ARC Prize Leaderboard – ARC‑AGI‑1&lt;/a&gt;, scoring &lt;strong&gt;about 87.5%&lt;/strong&gt; in its Deep Think preview — a very strong showing on a benchmark focused on abstract reasoning. &lt;strong&gt;ARC‑AGI‑1&lt;/strong&gt; (Abstraction &amp;amp; Reasoning Corpus for AGI) is designed to test &lt;em&gt;fluid intelligence&lt;/em&gt;: each task provides a few input/output examples, and models must infer underlying rules rather than rely on memorization. The benchmark emphasizes &lt;strong&gt;skill-acquisition efficiency&lt;/strong&gt;, rewarding reasoning and generalization over brute-force performance. (&lt;a href="https://arcprize.org/media/arc-prize-2024-technical-report.pdf"&gt;ARC Prize Technical Report&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;This 87.5% score suggests Gemini 3 Deep Think is effectively reasoning with abstract rules, not just pattern matching, and positions it as a model capable of structured, AGI‑style problem solving. Beyond ARC‑AGI‑1, Google has publicly confirmed that Gemini 3 Deep Think achieves &lt;strong&gt;45.1% on ARC‑AGI‑2&lt;/strong&gt;, a more challenging follow-up benchmark that also tests reasoning and code execution skills. (&lt;a href="https://blog.google/intl/en-africa/company-news/outreach-and-initiatives/a-new-era-of-intelligence-with-gemini-3/"&gt;Google Blog&lt;/a&gt;, &lt;a href="https://venturebeat.com/ai/google-unveils-gemini-3-claiming-the-lead-in-math-science-multimodal-and"&gt;VentureBeat&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;If these results hold, they represent a meaningful step forward: Gemini 3 is not just a larger LLM but a reasoning-first model capable of abstract problem solving. High performance on ARC‑AGI‑1 signals efficient learning and generalization — core aspects of intelligence that many benchmarks don’t test — and marks a clear signal that AI systems are beginning to handle tasks previously out of reach for conventional models.&lt;/p&gt;
</description>
      <pubDate>Tue, 25 Nov 2025 09:00:00 -0700</pubDate>
      <a10:updated>2025-11-25T09:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2659</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/ai-tutorial-build-a-free-gemini-ai-chat-agent-with-n8n/</link>
      <category>System.String[]</category>
      <title>AI Tutorial: Build a Free Gemini AI Chat Agent with n8n</title>
      <description>&lt;p&gt;n8n is a great tool for building useful AI Agents with minimal coding. And it even has a free version available if you want to self-host.&lt;/p&gt;
&lt;p&gt;One of &lt;a href="https://docs.n8n.io/advanced-ai/intro-tutorial/"&gt;the first tutorials n8n offers is building an n8n chat agent&lt;/a&gt;. Let’s go through that tutorial but with a twist: we’ll connect to Google’s Gemini instead of OpenAI. This is consistent with Mindfire’s goal of finding the best low-cost AI resources for smaller clients that can’t afford a huge AI bill but want to add AI to their applications. Plus, my version of the tutorial will walk you through step-by-step visually to make it as easy as possible.&lt;/p&gt;
&lt;p&gt;If necessary, &lt;a href="https://www.mindfiretechnology.com/blog/archive/ai-tutorial-installing-n8n-self-hosted-community-edition/"&gt;install n8n as discussed in the previous post&lt;/a&gt;. That post got you running a free working version of n8n running locally. Once that is done, you’re ready to create your first chat agent.&lt;/p&gt;
&lt;h2&gt;Creating a New Workflow&lt;/h2&gt;
&lt;p&gt;After running n8n locally (for me that is http://localhost:5678/) and registering or signing in (as covered in the previous post) you’ll see the overview screen.&lt;/p&gt;
&lt;p&gt;Click on “Create Workflow”:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture1.jpg" alt="Image 1. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Adding a Chat Trigger&lt;/p&gt;
&lt;p&gt;You’ll be taken to the workflow building screen where you’ll click on “Add first step…”:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture2.jpg" alt="Image 2. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Next you should search for ‘chat trigger’ and select it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture3.jpg" alt="Image 3. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;From here you just want to go “Back to canvas”:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture4.jpg" alt="Image 4. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;h2&gt;Adding an AI Agent&lt;/h2&gt;
&lt;p&gt;You’ll go back to the workflow screen where you will want to add another node:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture5.jpg" alt="Image 5. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Now search for “AI Agent”:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture6.jpg" alt="Image 6. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;This will take you to the Edit AI Agent View. We need to add a chat model to the chat agent, so select the “+” under the ‘chat model’:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture7.jpg" alt="Image 7. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;This will automatically take you back to the search screen but filtered on language models (notice the yellow box). You will search for the model of your choice. For this free demo, we’ll use Google Gemini so that it doesn’t cost anything:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture8.jpg" alt="Image 8. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;The end result should look something like this the below. Notice that for me it defaulted to the right Gemini credentials because I’ve set them up before. It’s smart enough to reuse them. Also note that control to edit the credentials:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture9.jpg" alt="Image 9. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;However, if you are doing this the first time, you’ll need to setup your credentials:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture10.jpg" alt="Image 10. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;This will take you to a screen to setup credentials:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture11.jpg" alt="Image 11. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Host for Gemini is &lt;a href="https://generativelanguage.googleapis.com/"&gt;https://generativelanguage.googleapis.com&lt;/a&gt; and you’ll need an API Key which you can get from &lt;a href="https://aistudio.google.com/"&gt;https://aistudio.google.com/&lt;/a&gt;. (See my tutorial for &lt;a href="https://www.mindfiretechnology.com/blog/archive/ai-tutorial-what-is-google-ai-studio/"&gt;Google’s AI Studio here&lt;/a&gt;.) &lt;/p&gt;
&lt;p&gt;There is a link to get an API key:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture12.jpg" alt="Image 12. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Now fill it into the credentials and it should work. I’ve had some problems with it not initially recognizing a valid Google model. Use models/gemini-2.5-flash (which should be the default). If it doesn’t like that and claims its invalid, I found it worked if I just went back to canvas and started over.&lt;/p&gt;
&lt;h2&gt;Connecting the Chat Trigger&lt;/h2&gt;
&lt;p&gt;It should automatically connect to the chat trigger by default, but I found I sometimes have to connect it manually like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture13.jpg" alt="Image 13. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;It should now look something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture14.jpg" alt="Image 14. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;h2&gt;Adding Persistence&lt;/h2&gt;
&lt;p&gt;Large Language Models, by default, have no persistence. Each time you type to them they have no idea what you previously were talking about. To fix this problem, we’ll need to add ‘memory’ by clicking the “+” under ‘memory’ and then select ‘simple memory:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture15.jpg" alt="Image 15. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;The default context window of 5 messages is fine for now, so go back to canvas:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture16.jpg" alt="Image 16. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;You should have a completed workflow that looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture17.jpg" alt="Image 17. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;One mistake I’ve made in the past is accidentally pinning the AI Agent so that it doesn’t execute when run. You can bang your head trying to figure out why your workflow isn’t working when really, it’s in test mode always returning default data. Be sure the pin icon is unselected here:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture18.jpg" alt="Image 18. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Let’s add a ‘system message’ which is just instructions for the chatbot to follow that allows you to lock in a specific style of chatbot:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture19.jpg" alt="Image 19. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;In this case let’s have the chatbot act like a politician that no matter what you ask it finds a way to change the subject. &lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture20.jpg" alt="Image 20. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;h2&gt;Save the final workflow&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture21.jpg" alt="Image 21. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;h2&gt;Chatting with Your AI Agent&lt;/h2&gt;
&lt;p&gt;Click the “Open Chat” button to test it out.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/build-a-free-gemini-ai-chat-agent-with-n8n_picture22.jpg" alt="Image 22. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Try asking your chatbot something and see how it responds. For me, it started off with:&lt;/p&gt;
&lt;p&gt;“Ah, the weather! A truly fascinating subject, wouldn't you agree? But you know, what's even more fascinating is the incredible progress we're making on the new community initiative. …”&lt;/p&gt;
&lt;p&gt;And then it went on to change the subject to talk about its own political initiatives.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;And that’s it!&lt;/strong&gt; You’ve just built your first n8n workflow — for free. But this is only the beginning.&lt;/p&gt;
&lt;p&gt;n8n is a powerful tool for automating almost anything. Imagine all those repetitive tasks you dread every day—gone. Need to schedule meetings automatically? Connect an AI agent to your calendar and email, and let it handle the back-and-forth for you. Overwhelmed by your inbox? Set up an AI-powered workflow to read messages, reply to urgent ones, and even text you when something truly needs your attention.&lt;/p&gt;
&lt;p&gt;With n8n and AI, you can turn busywork into background work—so you can focus on what really matters.&lt;/p&gt;
&lt;h2&gt;Ready to see what else is possible?&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/contact-us"&gt;Contact us at Mindfire Tech&lt;/a&gt;, and we’ll help you unlock the full potential of automation before your competitors do.&lt;/p&gt;
</description>
      <pubDate>Tue, 18 Nov 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-11-18T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2649</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/ai-tutorial-installing-n8n-self-hosted-community-edition/</link>
      <category>System.String[]</category>
      <title>AI Tutorial: Installing n8n Self Hosted Community Edition</title>
      <description>&lt;p&gt;I’m going to do some blog posts about using &lt;a href="https://n8n.io/"&gt;n8n&lt;/a&gt; to build an AI workflow. Consistent with our ‘open sourced’ / low-cost approach to Artificial Intelligence, I’m going to use the self-hosted community edition of n8n. This blog post will walk you through how to do the installation, in my case, for Windows. (Though it should work more or less the same for other operating systems.)&lt;/p&gt;
&lt;h2&gt;What is n8n?&lt;/h2&gt;
&lt;p&gt;n8n is a workflow-automation platform: it lets you connect apps, services, databases and APIs together, and automate sequences of tasks. It has a visual interface that lets you connect nodes together in a workflow graph:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/installing-n8n-self-hosted-community-edition_picture1.jpg" alt="Image 1. Will add description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;This may sound somewhat similar to &lt;a href="https://www.mindfiretechnology.com/blog/archive/haystack-google-and-gemma-a-tutorial/"&gt;Deepset’s Haystack&lt;/a&gt; and there is some overlap of functionality. For example, Haystack also has a graphical interface available to create its pipelines and it can also chain together nodes that call services. However, n8n can be used for any kind of workflow because n8n offers extensive API integrations to services (like Slack or Google Sheets) whereas Haystack is more oriented specifically to &lt;a href="https://www.mindfiretechnology.com/blog/archive/installing-pgvector-in-preparation-for-retrieval-augmented-generation/"&gt;integration with tools like PostgreSQL and pgvector&lt;/a&gt; or &lt;a href="https://d.docs.live.net/20bdfd902ae25866/Documents/Mindfire%20Tech%20Documents/Website%20Articles/Bruce%20Nielson%20Articles/Installing%20n8n%20Self%20Hosted%20Community%20Edition/haystack.deepset.ai/integrations/elasticsearch-document-store"&gt;elastic search&lt;/a&gt; as well as various Large Language Models. In other words, n8n is more general purpose and Haystack is more oriented towards AI.&lt;/p&gt;
&lt;p&gt;Consistent with Mindfire’s goal of finding low-cost AI solutions, I am going to go over how to install &lt;a href="https://docs.n8n.io/choose-n8n/"&gt;the self-hosted version of n8n&lt;/a&gt; using the free &lt;a href="https://docs.n8n.io/hosting/community-edition-features/"&gt;community edition&lt;/a&gt;. (&lt;a href="https://github.com/n8n-io"&gt;Github repo found here&lt;/a&gt;.) Though &lt;a href="https://docs.n8n.io/choose-n8n/"&gt;other versions are available&lt;/a&gt; including a paid plan hosted by n8n. This edition uses a &lt;a href="https://faircode.io/"&gt;fair-code license&lt;/a&gt;, so it is free to use.&lt;/p&gt;
&lt;p&gt;If you want to learn more, this &lt;a href="https://docs.n8n.io/try-it-out/"&gt;quick start guide&lt;/a&gt; is a great place to start. The n8n website includes a number of &lt;a href="https://docs.n8n.io/courses/"&gt;text&lt;/a&gt; and &lt;a href="https://docs.n8n.io/video-courses/"&gt;video&lt;/a&gt; courses to bring you up to speed.&lt;/p&gt;
&lt;h2&gt;Installing n8n Locally&lt;/h2&gt;
&lt;p&gt;First, if you don’t already have it installed, you’ll need Node.js. &lt;a href="https://www.mindfiretechnology.com/blog/archive/how-to-install-nodejs-for-windows/"&gt;See this blog post for details on how to install node.js&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, we need to install n8n itself. &lt;a href="https://docs.n8n.io/hosting/installation/npm/#try-n8n-with-npx"&gt;Go to this page for details&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’ll walk you through my own install of n8n onto a Windows machine.&lt;/p&gt;
&lt;p&gt;For Windows, open “Terminal” to get a Powershell command prompt.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/installing-n8n-self-hosted-community-edition_picture2.png" alt="Image 2. Will add description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;For our purposes we want to do the global install, so at the command prompt type:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install n8n -g
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/installing-n8n-self-hosted-community-edition_picture3.png" alt="Image 3. Will add description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;You’ll see an install take place:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/installing-n8n-self-hosted-community-edition_picture4.png" alt="Image 4. Will add description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Notice at the end that you have a localhost url:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://localhost:5678
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Run that in a browser and you’ll get the n8n web interface running locally and you’ll get a registration screen:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/installing-n8n-self-hosted-community-edition_picture5.png" alt="Image 5. Will add description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Go ahead and register and create a password. There will be a few extra screens you’ll have to pass through the first time such as answering a survey: &lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/installing-n8n-self-hosted-community-edition_picture6.png" alt="Image 6. Will add description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Finally, you’ll get to the actual n8n screen:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/installing-n8n-self-hosted-community-edition_picture7.png" alt="Image 7. Will add description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;You have now installed n8n and you’re ready to go with the community edition.&lt;/p&gt;
</description>
      <pubDate>Tue, 11 Nov 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-11-11T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2643</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/how-to-install-nodejs-for-windows/</link>
      <category>System.String[]</category>
      <title>How to Install Node.js (for Windows)</title>
      <description>&lt;p&gt;I’m going to explore &lt;a href="https://n8n.io/"&gt;n8n&lt;/a&gt; in future blog posts, so, go over how to set up an AI workflow. To keep this consistent with our ‘open sourced’ approach to AI, we’re going to do a self-hosted version of n8n using the free community edition. To install n8n you need to have Node.js already installed so that you can use the npm command. So, for this blog post we’re going to briefly go over how to install Node.js. This one is particularly easy to do and likely you already have it installed for some other purpose. But for completeness, let’s quickly cover it:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Update:
It's actually recommended that you use tools like &lt;a href="https://github.com/jasongin/nvs/releases/tag/v1.7.1"&gt;nvs&lt;/a&gt; (Windows) and &lt;a href="https://github.com/nvm-sh/nvm/releases"&gt;nvm&lt;/a&gt; (Mac &amp;amp; Linux)&lt;/p&gt;
&lt;p&gt;Additionally, there are very few (if any) packages that should be installed globally. Installing those as local dependencies can make a huge difference in your ability to move between projects that have different node versions and requirements. You can do this with the command parameter &lt;code&gt;--save-dev&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Example: &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;nvs use 20.15
&amp;gt;node --version
v20.15.0
&amp;gt;npm install typescript --save-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;hr /&gt;
&lt;p&gt;First, go to the node.js home page:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://nodejs.org/"&gt;https://nodejs.org/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You’ll see this page below. Click on the “Get Node.js” button:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/how-to-install-nodejs-for-windows_picture1.jpg" alt="image 1. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;That will take you to the download page (or you can just click the link below and go directly there):&lt;/p&gt;
&lt;p&gt;&lt;a href="https://nodejs.org/en/download"&gt;https://nodejs.org/en/download&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The download page offers you several different ways to download Node.js. Since we’re a Windows shop (and since Windows needs more AI love) I’m going to show you how to download the Windows installer, though feel free to change this to whatever operating system you prefer. First you need to select the operating system of your choice, which for me is Windows 64-bit:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/how-to-install-nodejs-for-windows_picture2.jpg" alt="image 2. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Then you click the “Windows Installer.msi” button to do the download:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/how-to-install-nodejs-for-windows_picture3.jpg" alt="image 3. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;After the download, click the browser download icon and select what you just downloaded and run it:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/how-to-install-nodejs-for-windows_picture4.jpg" alt="image 4. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;This starts the installer:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.mindfiretechnology.com/blog/media/how-to-install-nodejs-for-windows_picture5.jpg" alt="image 5. Will add more detailed description at a later date." /&gt;&lt;/p&gt;
&lt;p&gt;Now just take the defaults in the installer and you’re ready for the next step for installing n8n.&lt;/p&gt;
</description>
      <pubDate>Tue, 04 Nov 2025 12:00:00 -0700</pubDate>
      <a10:updated>2025-11-04T12:00:00-07:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2640</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/coxs-theorem-is-probability-theory-universal/</link>
      <category>System.String[]</category>
      <title>Cox's Theorem: Is Probability Theory Universal?</title>
      <description>&lt;p&gt;Most of life involves reasoning using incomplete information. Or put another way, it's about reasoning under uncertainty. Will it rain tomorrow? Should you carry an umbrella? Classical (propositional) logic handles things that are &lt;em&gt;exactly&lt;/em&gt; true or false — but it doesn’t tell you how to reason when you’re unsure.&lt;/p&gt;
&lt;p&gt;In our last several posts, &lt;a href="https://www.mindfiretechnology.com/blog/archive/from-certainty-to-belief-how-probability-extends-logic-part-3/"&gt;particularly this one&lt;/a&gt;, we talked about how probability theory is a generalization of boolean and propositional logic. That is to say, probability theory extends regular deductive logic and includes it; at least for boolean and propositional logic. (There is still an open question on if this is also true for First Order logic.) So probability theory gives us a way to reason with incomplete or uncertain information.&lt;/p&gt;
&lt;p&gt;But is probability theory just one possible way to reason under uncertainty? Or is it the &lt;em&gt;only&lt;/em&gt; way to do so?&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.cco.caltech.edu/~jimbeck/summerlectures/references/ProbabilityFrequencyReasonableExpectation.pdf"&gt;Cox’s theorem (R. T. Cox, 1946)&lt;/a&gt; attempts to answer that question. Put simply: if you want a system for &lt;strong&gt;plausible reasoning&lt;/strong&gt; that is sensible and consistent, you are inevitably led to probability theory. Edwin T. Jaynes championed and popularized this view; see his book (&lt;a href="https://amzn.to/4lY3WhX"&gt;&lt;em&gt;Probability Theory: The Logic of Science&lt;/em&gt;&lt;/a&gt;). &lt;/p&gt;
&lt;p&gt;If you want to dive into the originals, Cox’s paper and notes are available online:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.cco.caltech.edu/~jimbeck/summerlectures/references/ProbabilityFrequencyReasonableExpectation.pdf"&gt;Cox, &lt;em&gt;Probability, Frequency and Reasonable Expectation&lt;/em&gt; (American Journal of Physics, 1946). (PDF)&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://bayes.wustl.edu/Manual/cox-algebra.pdf"&gt;Cox, &lt;em&gt;The Algebra of Probable Inference&lt;/em&gt; (manuscript/PDF)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Cox%27s_theorem"&gt;Short overview / background: Cox’s theorem (Wikipedia)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;What we Demand of a Reasoner (According to Jayne)&lt;/h2&gt;
&lt;p&gt;Jaynes summarized Cox’s intuition in three crisp requirements. Jayne calls these 'desiderata' because they aren't axioms assumed to be true, but rather desires or goals we wish to constrain ourselves to. Despite the simplicity and even apparent 'obviousness' of these 'desiderata', these are the &lt;em&gt;only&lt;/em&gt; things we ask of a system that assigns degrees of plausibility to statements:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(I) Representation — degrees of plausibility are real numbers.&lt;/strong&gt;&lt;br /&gt;
You should be able to say, “this is more plausible than that” and encode that judgment with a single number:  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Everything gets a number.&lt;/em&gt; Any statement A has a plausibility &lt;code&gt;pl(A)&lt;/code&gt; (think: a slider).  
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Negation is linked.&lt;/em&gt; Knowing &lt;code&gt;pl(A)&lt;/code&gt; should tell you &lt;code&gt;pl(not A)&lt;/code&gt; (they’re not independent).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first requirement means you can always compare two (mutually exclusive) statements and say one is more plausible than the other, or they're equally plausible. On the surface this seems rather reasonable and almost inevitable. How else could plausibility work but via some way to ultimately decide between mutually exclusive options? And that implies there must be some way to weigh the options compared to their competitors. This isn't possible if you can't translate everything down to a single continuous value. &lt;/p&gt;
&lt;p&gt;The second sub point seems intuitively correct because knowing how plausible a statement is, you should automatically know how plausible its negation (the opposite statement) is. Suppose you think “it will rain” is somewhat likely: &lt;code&gt;pl(rain) = 0.8&lt;/code&gt; (80% plausible). Then, under Cox-like rules, the plausibility of “not rain” is determined: &lt;code&gt;pl(not rain) = 0.2&lt;/code&gt;. If you also learn “it’s cloudy” and that makes “rain” more plausible, the rules tell you &lt;em&gt;precisely&lt;/em&gt; how to combine the old plausibilities and the new evidence — and that combination must behave like Bayesian updating (i.e., use conditional probabilities).&lt;/p&gt;
&lt;p&gt;Despite the 'obviousness' of this desiderata, this is probably the most challenged requirement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(II) Qualitative correspondence with common sense.&lt;/strong&gt;&lt;br /&gt;
When information is crisp, the system should reduce to ordinary logic; small changes in evidence cause small changes in plausibility:  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Respect logic:&lt;/em&gt; logically equivalent statements get the same plausibility.  
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Continuity:&lt;/em&gt; tiny changes in input → tiny changes in output.  
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;The AND rule (decomposability):&lt;/em&gt; the plausibility of “A and B” should depend only on how plausible B is, and how plausible A would be &lt;em&gt;if&lt;/em&gt; B were true.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The continuity requirement seems sensible because you wouldn't expect a tiny piece of new information to cause a massive, sudden jump in your confidence about something. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(III) Consistency.&lt;/strong&gt;&lt;br /&gt;
Different, valid routes to the same conclusion must give the same answer; the rules must scale and compose:  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Universality:&lt;/em&gt; the same rules must work in any domain.  
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Non-contradiction:&lt;/em&gt; if a conclusion can be derived more than one way, all derivations agree.  
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Scalability:&lt;/em&gt; rules that make sense for one case should still make sense for many repeated or combined cases.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Your system of plausible reasoning should be consistent like formal logic (for our purposes that means propositional logic). For example, if two statements are logically equivalent (they mean the same thing), they should always have the same plausibility. Likewise, if something is always true by definition (a &amp;quot;tautology&amp;quot;), its plausibility should be at the maximum possible value.&lt;/p&gt;
&lt;p&gt;And the system should be universal – it shouldn't just apply to a specific type of problem or domain. You should be able to reason about anything, from unrelated events to complex scenarios, and the underlying rules should still hold. This is crucial for making the logic broadly applicable, just like the fundamental rules of mathematics or logic apply everywhere.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;One Probability Logic to Rule Them All?&lt;/h2&gt;
&lt;p&gt;Cox showed — and later expositions (Jaynes, others) made rigorous — that those seemingly mild, intuitive demands &lt;strong&gt;uniquely determine the algebra of plausible reasoning&lt;/strong&gt;. Up to a simple re-scaling, the plausibility numbers must obey the &lt;strong&gt;product rule&lt;/strong&gt; and the &lt;strong&gt;sum/negation rule&lt;/strong&gt;. After mapping to the usual 0–1 scale, those rules are exactly the familiar axioms of probability:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;P(A and B | C) = P(A | B,C) × P(B | C)&lt;/code&gt;  (product rule)  
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;P(A | C) + P(not A | C) = 1&lt;/code&gt;  (negation/sum rule)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is possible to come up with alternatives that might on the surface look a lot different. But there will always be a function that maps the results back to simple probability theory. The only alternative to this is a system that doesn't match those seemingly innocuous desiderata. (Which seems rather undesirable.) &lt;/p&gt;
&lt;p&gt;In short: If you accept Jaynes desiderata, &lt;strong&gt;probability theory is the only consistent extension of Boolean logic to uncertain situations.&lt;/strong&gt; Cox and Jayne argue that probability theory is not merely a tool for frequencies or gambling — it’s the uniquely rational way to handle degrees of belief in uncertain situations.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;So Why Does This Matter?&lt;/h2&gt;
&lt;p&gt;Cox's theorem is used to argue for several important points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Foundational justification for Bayesian reasoning.&lt;/strong&gt; Cox gives a principled reason to treat probabilities as degrees of belief (not only long-run frequencies).  
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Objectivity and consistency.&lt;/strong&gt; If you accept the desiderata, then any other scheme will give contradictory or “nonsensical” answers in some cases.  
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Applies to single events.&lt;/strong&gt; You don’t need repeatable trials — probability applies to unique hypotheses (e.g., “life exists on other planets”) because it codifies consistent belief, not frequency.  
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practical payoff.&lt;/strong&gt; The rules Cox forces on us are exactly the ones used in Bayesian inference, decision theory, and much of modern machine learning.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Conclusion and A Quick Caution&lt;/h2&gt;
&lt;p&gt;There are ongoing technical discussions (and a few edge-case counterexamples) in the literature about which exact axioms are needed. There are also a few (not so well known) alternative theories of plausibility that try out different desiderata -- such as not requiring plausibilities to be put in terms of a single real value. &lt;/p&gt;
&lt;p&gt;Still — this seems like a big breakthrough. It takes the idea we previously proved — that probability theory is an extension to propositional logic — and formalizes it and proves that under seemingly reasonable assumptions probability theory is the sole and only way to represent logical plausibilities.&lt;/p&gt;
&lt;p&gt;It is not an accident that Bayesian probabilities have come to dominate Machine Learning in recent years.&lt;/p&gt;
</description>
      <pubDate>Tue, 21 Oct 2025 12:00:00 -0600</pubDate>
      <a10:updated>2025-10-21T12:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2639</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/from-certainty-to-belief-how-probability-extends-logic-part-3/</link>
      <category>System.String[]</category>
      <title>From Certainty to Belief: How Probability Extends Logic - Part 3</title>
      <description>&lt;p&gt;Now we pull all those threads together and formally show that probability theory is an extension of propositional logic.&lt;/p&gt;
&lt;p&gt;As a reminder, in earlier posts we built a sequence of ideas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/aristotle-the-first-agi-researcher-a-primer-on-propositional-logic/"&gt;Aristotle: The First AGI Researcher&lt;/a&gt;&lt;/em&gt; - We covered the long road from Aristotle to today to try to simulate human thinking that led to the creation of deductive logics. In that post we also included a primer on propositional logic, the basis for computation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/the-atomization-of-thought/"&gt;The Atomization of Thought&lt;/a&gt;&lt;/em&gt; — We showed &lt;em&gt;any&lt;/em&gt; boolean function can be built out of nothing more than AND and NOT operators by using 'minterms'. That is to say, AND + NOT are &lt;em&gt;universal&lt;/em&gt; operators that can represent any other boolean operations or function of any complexity.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/ai-tutorial-one-logical-operator-to-rule-them-all/"&gt;One Logical Operator to Rule Them All&lt;/a&gt;&lt;/em&gt; — We showed that a single operator, NAND (or the pair NOT+AND together), is functionally complete: every propositional function can be built from them. It is a single universal boolean operator.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;From Certainty to Belief: How Probability Extends Logic - &lt;a href="https://www.mindfiretechnology.com/blog/archive/from-certainty-to-belief-how-probability-extends-logic-part-1/"&gt;Part 1&lt;/a&gt; and &lt;a href="https://www.mindfiretechnology.com/blog/archive/from-certainty-to-belief-how-probability-extends-logic-part-2/"&gt;Part 2&lt;/a&gt;&lt;/em&gt; — We introduced variables, marginals, conditional probability and Bayes’ Rule, and showed how probabilistic reasoning generalizes syllogistic logic as created by Aristotle.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now we pull all those threads together and formally show that probability theory is an extension to propositional logic. &lt;/p&gt;
&lt;p&gt;Because NOT and AND (or NAND) are enough to express any propositional formula, it is sufficient to show how to express NOT and AND &lt;strong&gt;in probability theory&lt;/strong&gt;. Once we can do that, probability theory can represent every propositional logical function — with logical entailment appearing as probability = 1.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;How to encode NOT and AND with probability&lt;/h2&gt;
&lt;p&gt;Encoding NOT and AND boolean operators into probability theory is remarkably easy. We treat logical statements as random variables taking values &lt;code&gt;tr&lt;/code&gt; (true) or &lt;code&gt;fa&lt;/code&gt; (false). Then we use probability &lt;em&gt;constraints&lt;/em&gt; to encode logical facts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NOT&lt;/strong&gt;&lt;br /&gt;
Logical &lt;code&gt;¬A&lt;/code&gt; (not A) is encoded by the probabilistic constraint:  &lt;strong&gt;p(A = tr) = 0&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;But recall that for two mutually exclusive values for a random variable they must marginalise (sum) to 1. So if &lt;code&gt;p(A = tr) = 0&lt;/code&gt; then &lt;code&gt;p(A = fa) = 1 - p(A = tr)&lt;/code&gt;. Since &lt;code&gt;p(A = tr) = 0&lt;/code&gt;, this means:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(A = fa) = 1 - p(A = tr) = 1 - 0 = 1&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;So &lt;code&gt;p(A = fa) = 1&lt;/code&gt;. This forces A to be false in every model/distribution that satisfies the KB. This allows us to represent the NOT operator using the notation of probability theory.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;AND&lt;/strong&gt;&lt;br /&gt;
Logical &lt;code&gt;A ∧ B&lt;/code&gt; (A and B) can be expressed in probability in a couple of very simple, equivalent ways — pick whichever feels more natural.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Joint form (one statement):&lt;/strong&gt; say the probability that both A and B are true is 1:&lt;br /&gt;
  &lt;strong&gt;p(A = tr, B = tr) = 1&lt;/strong&gt;&lt;br /&gt;
  This literally means: in every scenario allowed by our assumptions, A and B are both true.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Separate form (two statements):&lt;/strong&gt; say A is true with certainty and B is true with certainty:&lt;br /&gt;
  &lt;strong&gt;p(A = tr) = 1&lt;/strong&gt; and &lt;strong&gt;p(B = tr) = 1&lt;/strong&gt;&lt;br /&gt;
  Together these mean the same thing as the joint form — the only possible worlds left are those where A and B are both true.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Either way, you have encoded the logical AND using probabilities.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;NAND (single-operator route)&lt;/strong&gt;&lt;br /&gt;
&lt;code&gt;NAND(A,B)&lt;/code&gt; is just &lt;code&gt;NOT (A ∧ B)&lt;/code&gt;. To say “A NAND B is true” you can rule out the assignment where both A and B are true:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;p(A = tr, B = tr) = 0&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That says: the situation where both A and B are true has zero probability — it cannot happen. Since NAND is universal, you can use this single kind of constraint to build any Boolean function if you want to work from one operator only.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Tiny worked example — &lt;code&gt;A ∧ ¬B&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We want to encode the formula “A and not B”.&lt;/p&gt;
&lt;p&gt;Two equivalent, simple encodings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Joint constraint:&lt;/strong&gt; require the single assignment where A is true and B is false to be the only possibility:&lt;br /&gt;
  &lt;strong&gt;p(A = tr, B = fa) = 1&lt;/strong&gt;&lt;br /&gt;
  Read plainly: the only allowed scenario is A true and B false — so the formula is true in every allowed scenario.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Atom constraints:&lt;/strong&gt; say A is certain and B is impossible:&lt;br /&gt;
  &lt;strong&gt;p(A = tr) = 1&lt;/strong&gt; and &lt;strong&gt;p(B = tr) = 0&lt;/strong&gt;&lt;br /&gt;
  That also leaves only worlds where A is true and B is false, so again the formula is true in every allowed scenario.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Either approach works.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Logical connectives — Boolean version and probabilistic version&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;NOT (¬)&lt;/strong&gt;&lt;br /&gt;
  &lt;strong&gt;Boolean:&lt;/strong&gt; If &lt;code&gt;P&lt;/code&gt; is true, then &lt;code&gt;¬P&lt;/code&gt; is false; if &lt;code&gt;P&lt;/code&gt; is false, then &lt;code&gt;¬P&lt;/code&gt; is true.&lt;br /&gt;
  &lt;strong&gt;Probability:&lt;/strong&gt; encode &lt;code&gt;¬P&lt;/code&gt; by forcing &lt;code&gt;P&lt;/code&gt; to be false:&lt;br /&gt;
  &lt;code&gt;p(P = tr) = 0&lt;/code&gt;  (equivalently &lt;code&gt;p(P = fa) = 1&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AND (∧)&lt;/strong&gt;&lt;br /&gt;
  &lt;strong&gt;Boolean:&lt;/strong&gt; &lt;code&gt;P ∧ Q&lt;/code&gt; is true only if both &lt;code&gt;P&lt;/code&gt; and &lt;code&gt;Q&lt;/code&gt; are true.&lt;br /&gt;
  &lt;strong&gt;Probability (joint form):&lt;/strong&gt; require both to be true with certainty:&lt;br /&gt;
  &lt;code&gt;p(P = tr, Q = tr) = 1&lt;/code&gt;.&lt;br /&gt;
  &lt;strong&gt;Probability (separate form):&lt;/strong&gt; require each true with certainty:&lt;br /&gt;
  &lt;code&gt;p(P = tr) = 1&lt;/code&gt; and &lt;code&gt;p(Q = tr) = 1&lt;/code&gt; (together these imply the joint form).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;OR (∨)&lt;/strong&gt;&lt;br /&gt;
  &lt;strong&gt;Boolean:&lt;/strong&gt; &lt;code&gt;P ∨ Q&lt;/code&gt; is true if at least one of &lt;code&gt;P&lt;/code&gt; or &lt;code&gt;Q&lt;/code&gt; is true.&lt;br /&gt;
  &lt;strong&gt;Probability (direct):&lt;/strong&gt; require that at least one is true with certainty:&lt;br /&gt;
  &lt;code&gt;p(P = tr or Q = tr) = 1&lt;/code&gt; (read literally: the probability that P or Q is true equals 1).&lt;br /&gt;
  &lt;strong&gt;Probability (algebraic):&lt;/strong&gt; using inclusion–exclusion:&lt;br /&gt;
  &lt;code&gt;p(P = tr) + p(Q = tr) - p(P = tr, Q = tr) = 1&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IMPLIES (→)&lt;/strong&gt;&lt;br /&gt;
  &lt;strong&gt;Boolean:&lt;/strong&gt; &lt;code&gt;P → Q&lt;/code&gt; is false exactly when &lt;code&gt;P&lt;/code&gt; is true and &lt;code&gt;Q&lt;/code&gt; is false; otherwise it’s true.&lt;br /&gt;
  &lt;strong&gt;Probability (for implication-as-certainty):&lt;/strong&gt; either of these equivalent encodings works:  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Forbid the counterexample: &lt;code&gt;p(P = tr, Q = fa) = 0&lt;/code&gt;.  
&lt;/li&gt;
&lt;li&gt;Or express as a conditional: &lt;code&gt;p(Q = tr | P = tr) = 1&lt;/code&gt;.&lt;br /&gt;
(Both say: whenever P is true, Q must also be true. However, when using conditional form &lt;code&gt;p(Q = tr | P = tr) = 1&lt;/code&gt;, this implicitly assumes &lt;code&gt;p(P = tr) &amp;gt; 0&lt;/code&gt;; if &lt;code&gt;p(P = tr) = 0&lt;/code&gt; prefer the joint form &lt;code&gt;p(P = tr, Q = fa) = 0&lt;/code&gt;.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;EQUIVALENT (↔)&lt;/strong&gt;&lt;br /&gt;
  &lt;strong&gt;Boolean:&lt;/strong&gt; &lt;code&gt;P ↔ Q&lt;/code&gt; is true only when &lt;code&gt;P&lt;/code&gt; and &lt;code&gt;Q&lt;/code&gt; share the same truth value (both true or both false).&lt;br /&gt;
  &lt;strong&gt;Probability (joint form):&lt;/strong&gt; require that only the matching pairs are true:&lt;br /&gt;
  &lt;code&gt;p(P = tr, Q = tr) + p(P = fa, Q = fa) = 1&lt;/code&gt;.&lt;br /&gt;
  &lt;strong&gt;Probability (conditional form):&lt;/strong&gt; equivalently, require both directions as certain:&lt;br /&gt;
  &lt;code&gt;p(P = tr | Q = tr) = 1&lt;/code&gt; and &lt;code&gt;p(P = tr | Q = fa) = 0&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;Note that:
- Saying &lt;code&gt;p(...)=1&lt;/code&gt; is the probabilistic way to say “this is certainly true” (the logical 1).&lt;br /&gt;
- Saying &lt;code&gt;p(...)=0&lt;/code&gt; is the probabilistic way to say “this cannot happen” (the logical 0).&lt;br /&gt;
- You can mix joint probabilities (e.g., &lt;code&gt;p(P,Q)&lt;/code&gt;) and conditionals (e.g., &lt;code&gt;p(Q | P)&lt;/code&gt;) — they are equivalent ways to express the same logical constraint.&lt;br /&gt;
- Once you can express &lt;code&gt;NOT&lt;/code&gt; and &lt;code&gt;AND&lt;/code&gt; this way, you can build &lt;code&gt;OR&lt;/code&gt;, &lt;code&gt;IMPLIES&lt;/code&gt;, &lt;code&gt;EQUIV&lt;/code&gt;, and therefore any propositional formula, by combining the probabilistic encodings above.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;As this post shows, probability theory can represent any boolean or propositional logical statement. Thus probability theory extends boolean and propositional logic. That is to say, it generalizes boolean and propositional logic — the 0/1 case of probability theory recovers ordinary logic.&lt;/p&gt;
&lt;p&gt;But what about First Order logic? Also, is there a way to formally prove this?&lt;/p&gt;
</description>
      <pubDate>Tue, 14 Oct 2025 09:00:00 -0600</pubDate>
      <a10:updated>2025-10-14T09:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2638</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/from-certainty-to-belief-how-probability-extends-logic-part-2/</link>
      <category>System.String[]</category>
      <title>From Certainty to Belief: How Probability Extends Logic - Part 2</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/from-certainty-to-belief-how-probability-extends-logic-part-1/"&gt;In our last post&lt;/a&gt;, we built an intuition for why probability theory might be thought of as an extension of deductive logic. In an earlier post we went over &lt;a href="https://www.mindfiretechnology.com/blog/archive/dice-rolls-coin-flips-and-death-by-asteroid-a-probability-refresher/"&gt;the basics of probability theory&lt;/a&gt;. (And &lt;a href="https://www.mindfiretechnology.com/blog/archive/aristotle-the-first-agi-researcher-a-primer-on-propositional-logic/"&gt;in an even earlier post, we went over the basics or propositional logic&lt;/a&gt;.) &lt;/p&gt;
&lt;p&gt;Let's now see if we can formalize our intuition and show that it is possible to do deductive logic using only probability theory. To do this, I'm going to use two fun examples taken from David Barber’s textbook &lt;em&gt;&lt;a href="https://amzn.to/3JskQrc"&gt;Bayesian Reasoning and Machine Learning&lt;/a&gt;&lt;/em&gt; (we follow his Example 1.5 and 1.6 on p. 11-12). We'll work them through his examples &lt;em&gt;step by step&lt;/em&gt;, explicitly showing which rules or probability theory we use at every line. The goal is to make the connection between logic and probability completely transparent.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Quick reminder of the rules of probability theory&lt;/h2&gt;
&lt;p&gt;Rules all taken from this post. &lt;link&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Variables &amp;amp; states&lt;/strong&gt;&lt;br /&gt;
   A variable (e.g. &lt;code&gt;A&lt;/code&gt;, &lt;code&gt;F&lt;/code&gt;, &lt;code&gt;T&lt;/code&gt;) can take &lt;code&gt;tr&lt;/code&gt; (true) or &lt;code&gt;fa&lt;/code&gt; (false).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Joint symmetry&lt;/strong&gt;&lt;br /&gt;
   &lt;code&gt;p(x, y) = p(y, x)&lt;/code&gt;. The joint probability is symmetric.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Conditional probability (definition)&lt;/strong&gt;&lt;br /&gt;
   &lt;code&gt;p(x | y) = p(x, y) / p(y)&lt;/code&gt;  — equivalently &lt;code&gt;p(x, y) = p(x | y) × p(y)&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Marginalisation (summing out)&lt;/strong&gt;&lt;br /&gt;
   &lt;code&gt;p(x) = sum_y p(x, y)&lt;/code&gt; — in two states: &lt;code&gt;p(x) = p(x, y=tr) + p(x, y=fa)&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Bayes’ Rule&lt;/strong&gt; (derived from the above)&lt;br /&gt;
   &lt;code&gt;p(x | y) = [p(y | x) × p(x)] / p(y)&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Conditional independence&lt;/strong&gt; (when stated)&lt;br /&gt;
   &lt;code&gt;p(z | x, y) = p(z | y)&lt;/code&gt; means once &lt;code&gt;y&lt;/code&gt; is known, &lt;code&gt;x&lt;/code&gt; gives no extra info about &lt;code&gt;z&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Example 1 — Aristotle’s syllogism (Barber Example 1.5)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Informal statement:&lt;/strong&gt;  
&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“All apples are fruit.&amp;quot; &lt;/li&gt;
&lt;li&gt;&amp;quot;All fruit grow on trees.&amp;quot; &lt;/li&gt;
&lt;li&gt;&amp;quot;Therefore, all apples grow on trees.”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Variables:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;A&lt;/code&gt; = &amp;quot;is an apple&amp;quot; (&lt;code&gt;A = tr&lt;/code&gt; means it is an apple)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;F&lt;/code&gt; = &amp;quot;is a fruit&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T&lt;/code&gt; = &amp;quot;grows on trees&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Given (probabilistic form of the premises):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;p(F = tr | A = tr) = 1&lt;/code&gt;  (All apples are fruit.)  
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p(T = tr | F = tr) = 1&lt;/code&gt;  (All fruits grow on trees.)  
&lt;/li&gt;
&lt;li&gt;Conditional independence assumption: &lt;code&gt;p(T | A, F) = p(T | F)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note that #3 means that if you know fruit and apples grow on trees that since an apple is a fruit this is the same as saying just fruit grows on trees. We're simply showing that you learn nothing more given A if you already have F.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; show &lt;code&gt;p(T = tr | A = tr) = 1&lt;/code&gt;. &lt;/p&gt;
&lt;p&gt;That is to say, show that if you know something is an apple, you know it grew on a tree given your premises. (Exactly like deductive logic.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step-by-step derivation (every step labeled with the rule used):&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Start with the target: &lt;code&gt;p(T = tr | A = tr)&lt;/code&gt;.&lt;br /&gt;
   (Goal)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use &lt;strong&gt;marginalisation&lt;/strong&gt; over &lt;code&gt;F&lt;/code&gt; (rule 4):&lt;br /&gt;
   &lt;code&gt;p(T = tr | A = tr) = Σ_s∈{tr,fa} p(T = tr, F = s | A = tr)&lt;/code&gt;.&lt;br /&gt;
   (We sum over the unobserved variable &lt;code&gt;F&lt;/code&gt; to account for both states &lt;code&gt;s = tr&lt;/code&gt; and &lt;code&gt;s = fa&lt;/code&gt;.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use the &lt;strong&gt;definition of joint/conditional&lt;/strong&gt; (rule 3):&lt;br /&gt;
   &lt;code&gt;p(T = tr, F = s | A = tr) = p(T = tr | F = s, A = tr) × p(F = s | A = tr)&lt;/code&gt;.  
&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Therefore:&lt;br /&gt;
   &lt;code&gt;p(T = tr | A = tr) = Σ_s p(T = tr | F = s, A = tr) × p(F = s | A = tr)&lt;/code&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Apply the &lt;strong&gt;conditional independence&lt;/strong&gt; assumption &lt;code&gt;p(T | A, F) = p(T | F)&lt;/code&gt; (given):&lt;br /&gt;
   Replace &lt;code&gt;p(T = tr | F = s, A = tr)&lt;/code&gt; with &lt;code&gt;p(T = tr | F = s)&lt;/code&gt;.  
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now:&lt;br /&gt;
   &lt;code&gt;p(T = tr | A = tr) = Σ_s p(T = tr | F = s) × p(F = s | A = tr)&lt;/code&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Write out the explicit two-term sum (&lt;code&gt;s = fa&lt;/code&gt; and &lt;code&gt;s = tr&lt;/code&gt;):&lt;br /&gt;
   &lt;code&gt;p(T = tr | A = tr) = p(T = tr | F = fa) × p(F = fa | A = tr) + p(T = tr | F = tr) × p(F = tr | A = tr)&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Use premise 1: &lt;code&gt;p(F = tr | A = tr) = 1&lt;/code&gt;. By &lt;strong&gt;normalisation&lt;/strong&gt;, &lt;code&gt;p(F = fa | A = tr) = 0&lt;/code&gt;.  
&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is because probability of all possible events must sum to 1 under probability theory. So if &lt;code&gt;p(F = tr | A = tr) = 1&lt;/code&gt; then its inverse &lt;code&gt;p(F = fa | A = tr) = 0&lt;/code&gt;. Or put in plain English, if it is certain that something being an apple is always a fruit too, then it must be equally certain that given something is an apple it must be false that it is not a fruit. (Double negative there.)&lt;/p&gt;
&lt;p&gt;Substitute:&lt;br /&gt;
   &lt;code&gt;p(T = tr | A = tr) = p(T = tr | F = fa) × 0 + p(T = tr | F = tr) × 1&lt;/code&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use premise 2: &lt;code&gt;p(T = tr | F = tr) = 1&lt;/code&gt;.&lt;br /&gt;
   Since the first term is multiplied by &lt;code&gt;0&lt;/code&gt;, p(T = tr | F = fa) drops out, leaving:&lt;br /&gt;
   &lt;code&gt;p(T = tr | A = tr) = 0 + 1 × 1 = 1&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Note that we do not need to know what the probability p(T = tr | F = fa) (that is, the probability if you know something isn't a fruit that it grows on a tree) to resolve this problem thanks to it dropping out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conclusion (Example 1):&lt;/strong&gt; &lt;code&gt;p(T = tr | A = tr) = 1&lt;/code&gt;.&lt;br /&gt;
In words: given the premises and conditional independence, probabilistic rules yield the conclusion: &amp;quot;all apples grow on trees&amp;quot; just like the classical logical syllogism. &lt;/p&gt;
&lt;p&gt;Barber points out that this reasoning is a form of resolution, a kind of transitivity: from A ⇒ F and F ⇒ T, we can infer A ⇒ T.&lt;/p&gt;
&lt;p&gt;Or put simply, it is possible to use the probability calculus to do regular logical resolution and work out a syllogism.&lt;/p&gt;
&lt;h2&gt;Example 2 — The contrapositive (Barber Example 1.6)&lt;/h2&gt;
&lt;p&gt;This example is important because Karl Popper noted that the contrapositive (aka Modus Tollens) is the basis for the logic of scientific reasoning. (That we 'falsify' our theories rather than verify them.) So showing that we can do the same thing with probability theory will show that probability theory is also the logic of science. (Or at least can be.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Informal statement:&lt;/strong&gt; &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;quot;If A then B&amp;quot; implies &amp;quot;If not B then not A.&amp;quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Variables:&lt;/strong&gt;
- &lt;code&gt;A&lt;/code&gt; and &lt;code&gt;B&lt;/code&gt; are boolean variables (tr/fa).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Given (probabilistic premise):&lt;/strong&gt;
- &lt;code&gt;p(B = tr | A = tr) = 1&lt;/code&gt;.  (If A is true then B is certainly true.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Goal:&lt;/strong&gt; show &lt;code&gt;p(A = fa | B = fa) = 1&lt;/code&gt;. (That is, show that given B to be false that you know for sure that A is also false.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step-by-step derivation:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Start from normalization for &lt;code&gt;A&lt;/code&gt; conditioned on &lt;code&gt;B = fa&lt;/code&gt;:&lt;br /&gt;
   &lt;code&gt;p(A = fa | B = fa) = 1 - p(A = tr | B = fa)&lt;/code&gt;.&lt;br /&gt;
   Because p(A=fa | B) + p(A=tr | B) = 1. (i.e. the total of all possibilities must sum to 1)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;We will show &lt;code&gt;p(A = tr | B = fa) = 0&lt;/code&gt;. If that holds, the left side equals 1.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;From the premise &lt;code&gt;p(B = tr | A = tr) = 1&lt;/code&gt;, take the complement to get:&lt;br /&gt;
   &lt;code&gt;p(B = fa | A = tr) = 1 - p(B = tr | A = tr) = 1 - 1 = 0&lt;/code&gt;.&lt;br /&gt;
   (If A is true, B cannot be false.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Now use &lt;strong&gt;Bayes’ Rule&lt;/strong&gt; to express &lt;code&gt;p(A = tr | B = fa)&lt;/code&gt; in terms of &lt;code&gt;p(B = fa | A = tr)&lt;/code&gt;:&lt;br /&gt;
   &lt;code&gt;p(A = tr | B = fa) = [ p(B = fa | A = tr) × p(A = tr) ] / p(B = fa)&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The numerator contains &lt;code&gt;p(B = fa | A = tr)&lt;/code&gt;, which we just showed equals &lt;code&gt;0&lt;/code&gt;.  
&lt;/li&gt;
&lt;li&gt;Therefore the numerator is &lt;code&gt;0 × p(A = tr) = 0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So the fraction equals &lt;code&gt;0 / p(B = fa) = 0&lt;/code&gt; (provided &lt;code&gt;p(B = fa) &amp;gt; 0&lt;/code&gt;). The denominator can be ignored. Note that if &lt;code&gt;p(B = fa) = 0&lt;/code&gt;, then &lt;code&gt;p(B = tr) = 1&lt;/code&gt; and the contrapositive is vacuously satisfied anyhow because given p(B = tr) it must be the case that p(B = tr | A = tr) regardless.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Thus &lt;code&gt;p(A = tr | B = fa) = 0&lt;/code&gt;. Plugging this into step 1:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;p(A = fa | B = fa) = 1 - 0 = 1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conclusion (Example 2):&lt;/strong&gt; &lt;code&gt;p(A = fa | B = fa) = 1&lt;/code&gt;.  
&lt;/p&gt;
&lt;p&gt;Or in other words, if B is false than A must also be false. This matches the logical contrapositive (modus tollens): from &amp;quot;If A then B&amp;quot; we infer &amp;quot;If not B then not A.&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Why these worked and what they teach us&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Every step used only the standard rules of probability theory&lt;/strong&gt;: conditional probability, marginalisation (summing), joint expansion, Bayes’ rule and (where stated) conditional independence. I called out which rule was used at each line so you can trace the logic directly back to the primer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;When premises are certainties (probability = 1), probabilistic reasoning reproduces classical logic.&lt;/strong&gt; In the examples above the premises used conditional probabilities equal to 1; that forces certain conditional and marginal terms to be 0 or 1 and collapses the algebra to the logical conclusions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Probability can reproduce Aristotle's Syllogisms .&lt;/strong&gt; We specifically choose to reproduce Aristotle's Syllogisms so that we could prove that probability theory was a generalization of syllogistic logic.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Probability is strictly more general than propositional or Boolean logic.&lt;/strong&gt; If any premise had been less than 1 (for example &lt;code&gt;p(B = tr | A = tr) = 0.9&lt;/code&gt;), the same algebra would produce posterior probabilities strictly between 0 and 1 instead of 0 or 1. That is the strength of the probabilistic approach: it handles degrees of belief as naturally as it handles certainties.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This demonstration goes beyond our previous intuition that probability theory extends classical deductive logic -- at least for syllogistic logic so far. But does this represent a formal proof? Can probability theory represent all of propositional logic? And what about First Order logic? &lt;/p&gt;
&lt;p&gt;We will consider these questions in future blog posts.&lt;/p&gt;
</description>
      <pubDate>Tue, 07 Oct 2025 09:00:00 -0600</pubDate>
      <a10:updated>2025-10-07T09:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2637</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/from-certainty-to-belief-how-probability-extends-logic-part-1/</link>
      <category>System.String[]</category>
      <title>From Certainty to Belief: How Probability Extends Logic - Part 1</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/dice-rolls-coin-flips-and-death-by-asteroid-a-probability-refresher/"&gt;In our previous post&lt;/a&gt;, we gave an introduction to probability theory. &lt;a href="https://www.mindfiretechnology.com/blog/archive/aristotle-the-first-agi-researcher-a-primer-on-propositional-logic/"&gt;In an even earlier post&lt;/a&gt;, we went over the basics of propositional logic — starting with the logic of Aristotle, moving on to Boolean logic, and finally showing the link to propositional logic.  
&lt;/p&gt;
&lt;p&gt;But is there any relationship between probability theory and deductive logic? In fact, there is! Boolean logic and propositional logic can be thought of as a special case of probability theory.  
&lt;/p&gt;
&lt;p&gt;This may seem quite strange at first. When we think about reasoning, our minds often jump to logic: if certain statements are true, then other statements must be true. This is the bedrock of deductive logic — a system built on clear-cut, binary truths: something is either true or false.  
&lt;/p&gt;
&lt;p&gt;What could logic possibly have to do with something as squishy and uncertain as probability theory? Think back to the examples we used in the previous post on probability theory. If you roll a die, you don't know for certain the outcome. But you do know that rolling two fair dice and scoring a total of 12 has only a 1 in 36 chance of happening. So if you needed to make a rational decision based on those odds, you could calculate the rationally correct choice — even though there’s no certainty like there is in deductive logic.  
&lt;/p&gt;
&lt;p&gt;So just intuitively, there does seem to be something at least &lt;em&gt;similar&lt;/em&gt; between deductive logic and probability theory. Both can be used to make rational decisions.  
&lt;/p&gt;
&lt;p&gt;Given this intuition, could it be possible that there is some sort of formal relationship between deductive logic and probability theory? Or is this intuition misguided?  
&lt;/p&gt;
&lt;h2&gt;Deductive Logic: The World of Absolutes&lt;/h2&gt;
&lt;p&gt;To explore this further, let's start with traditional deductive logic (either propositional or Boolean logic works here), where statements are assigned one of two values: True or False. If &amp;quot;A implies B&amp;quot; is true, and &amp;quot;A is true&amp;quot; is true, then &amp;quot;B is true&amp;quot; must logically follow. There’s no room for &amp;quot;A is probably true&amp;quot; or &amp;quot;B is sometimes true.&amp;quot; It’s a system of absolute certainties.  
&lt;/p&gt;
&lt;p&gt;But our real world is rarely so black and white. We encounter situations where information is incomplete, observations are noisy, and conclusions are tentative. This is where probability theory offers a richer language that allows more flexible reasoning.  
&lt;/p&gt;
&lt;h2&gt;Probability Theory: Quantifying Uncertainty&lt;/h2&gt;
&lt;p&gt;Probability theory provides a framework for managing uncertainty. When you roll a fair die, you don't know what outcome you'll get — but you do have a rational explanation for why a 12 on two fair dice will only happen 1 in 36 throws. So you can assign a confidence to the outcome even though it’s uncertain.  
&lt;/p&gt;
&lt;p&gt;In fact, this can also be true for a non-random event. In our previous post, we considered the “probability” of an asteroid hitting the Earth and even put a number on that event despite it being non-random. Re-imagining probability theory as a sort of &lt;em&gt;plausibility calculus&lt;/em&gt; (more on this in future posts) allows us to use the same mathematical rigor for both uncertainty due to randomness in nature and uncertainty due to our ignorance.  
&lt;/p&gt;
&lt;p&gt;Imagine an extension to propositional logic where, instead of only restricting yourself to the two values of 1 = True and 0 = False, you allow any continuous value between 0 and 1. If the value is 0.5, you have no reason to prefer assuming the event in question is true or false (say, you don’t know if a giant asteroid will hit the Earth this year). If the value is 1, you currently accept the event as true (you’ve observed an asteroid heading toward Earth and calculated it’s on a collision course). If the value is 0, you currently accept the event as false (you’ve scanned the sky and there are no asteroids on a collision course this year).  
&lt;/p&gt;
&lt;p&gt;Now say you have no evidence to work with other than the fact that asteroids hit the Earth once every 500,000 years (as discussed in the previous post &lt;link&gt;). Then your best estimate is that there is a 0.0002% chance that a giant asteroid will hit the Earth this year.  
&lt;/p&gt;
&lt;p&gt;Notice how allowing continuous values from 0 to 1 lets us mathematically express these intuitive ideas about the “probability” of a giant asteroid smashing into the Earth given some set of observations. This is true even though giant asteroids are not random events.  
&lt;/p&gt;
&lt;p&gt;What, then, are we expressing if not the probability of a random event? There is considerable debate on this point, but the most obvious answer is that we’re expressing the plausibility of an event or statement being true given what we currently know — and what we currently don’t know. This is sometimes called, by Bayesians, &lt;strong&gt;degrees of belief&lt;/strong&gt; (a term I don’t object to, even though I think Bayesians only have part of the truth about probability theory).  
&lt;/p&gt;
&lt;p&gt;When viewed this way, it seems intuitive that probability theory is an extension of deductive logic. Deductive logic is just a special case of probability theory that allows only True (1) and False (0), whereas probability theory allows any value between 0 and 1.  
&lt;/p&gt;
&lt;h2&gt;Probability Theory as an Extension of Deductive Logic&lt;/h2&gt;
&lt;p&gt;Can we show formally that our intuition is correct — that deductive logic is a special case of probability theory, and that probability theory extends deductive logic?  
&lt;/p&gt;
&lt;p&gt;In my next post, I’ll go over two excellent examples from David Barber’s &lt;em&gt;&lt;a href="https://amzn.to/4oBqnvJ"&gt;Bayesian Reasoning and Machine Learning&lt;/a&gt;&lt;/em&gt; that demonstrate exactly that.  
&lt;/p&gt;
</description>
      <pubDate>Tue, 30 Sep 2025 09:00:00 -0600</pubDate>
      <a10:updated>2025-09-30T09:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2636</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/dice-rolls-coin-flips-and-death-by-asteroid-a-probability-refresher/</link>
      <category>System.String[]</category>
      <title>Dice Rolls, Coin Flips, and Death By Asteroid: A Probability Refresher</title>
      <description>&lt;h2&gt;Introduction: Randomness, Belief, and Asteroids&lt;/h2&gt;
&lt;p&gt;Imagine flipping a coin or rolling a die—those feel &lt;em&gt;truly random&lt;/em&gt;. Every flip and roll is fresh, unpredictable, and fair. When we say there is a 50% chance of getting heads or “1‑in‑6” odds of rolling a six, probability captures the genuine randomness of the experiment.&lt;/p&gt;
&lt;p&gt;But what about something like &lt;strong&gt;Earth getting hit by a giant asteroid&lt;/strong&gt;? That event doesn’t feel random in the same way—it’s a unique, one‑off cosmic occurrence based on deterministic laws. There is either an asteroid heading to earth that is going to hit us this year or there isn't. So where does probability come into play? &lt;/p&gt;
&lt;p&gt;When we assign a probability to it, we’re really modeling our &lt;strong&gt;uncertainty&lt;/strong&gt;—our lack of exact knowledge—not a repeatable random process like a coin flip.&lt;/p&gt;
&lt;p&gt;For example, consider asteroids around &lt;strong&gt;1 km in diameter&lt;/strong&gt;—the kinds that could trigger widespread devastation or even global catastrophe. According to the Catalina Sky Survey, such an object strikes Earth on average &lt;strong&gt;once every 500,000 years&lt;/strong&gt; (&lt;a href="https://catalina.lpl.arizona.edu/faq/how-often-do-asteroids-strike-earth"&gt;Catalina Sky Survey FAQ&lt;/a&gt;). That translates to an &lt;strong&gt;annual probability&lt;/strong&gt; of approximately:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1 / 500,000 = 0.000002 (or 0.0002%)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this case, probability is less about dice‑rolling-style pure randomness and more about representing our &lt;strong&gt;ignorance and expectations based on long‑term historical frequencies&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In the sections that follow, we’ll revisit classic probability concepts using coin flips and dice rolls—but we’ll also carry forward that perspective: probability is sometimes about real randomness, and sometimes about modeling how much we don’t know.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Variables and States&lt;/h2&gt;
&lt;p&gt;Let’s start with a simple example: flipping a fair coin. A coin can be in one of two states, either heads or tails. &lt;/p&gt;
&lt;p&gt;We can represent this experiment using a &lt;strong&gt;random variable&lt;/strong&gt;. Let’s call it &lt;code&gt;c&lt;/code&gt;, for coin. The set of values that &lt;code&gt;c&lt;/code&gt; can take is called its &lt;strong&gt;domain&lt;/strong&gt;, and we write this as:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;dom(c) = {heads, tails}&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This means that the variable &lt;code&gt;c&lt;/code&gt; can be in one of two &lt;strong&gt;states&lt;/strong&gt;: &lt;code&gt;heads&lt;/code&gt; or &lt;code&gt;tails&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If the coin is fair, we assign equal probabilities to each outcome:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(c = heads) = 0.5&lt;/strong&gt;&lt;br /&gt;
&lt;strong&gt;p(c = tails) = 0.5&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So when we say something like &lt;code&gt;p(heads) = 0.5&lt;/code&gt;, we’re using shorthand for &lt;code&gt;p(c = heads)&lt;/code&gt;— the probability that the coin lands on heads.&lt;/p&gt;
&lt;p&gt;The important thing here is that we’re treating the coin as a variable (&lt;code&gt;c&lt;/code&gt;), and the results it can take on (&lt;code&gt;heads&lt;/code&gt; or &lt;code&gt;tails&lt;/code&gt;) as states of that variable.&lt;/p&gt;
&lt;p&gt;An &lt;em&gt;event&lt;/em&gt; is an expression about a random variable such as &lt;em&gt;two heads in a row&lt;/em&gt; or &lt;em&gt;two heads out of three flips&lt;/em&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Summing Over Variables&lt;/h2&gt;
&lt;p&gt;Another important rule in probability is that the total probability over all possible states of a variable must add up to 1. This is called the &lt;strong&gt;normalisation condition&lt;/strong&gt;. When you flip a coin you know that it must either land on heads or tails, so the total probability of both combined must add up to 1.0, meaning there is a 100% chance (certainty) it will be one of the two sides.&lt;/p&gt;
&lt;p&gt;For our coin variable &lt;code&gt;c&lt;/code&gt;, this means:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(c = heads) + p(c = tails) = 1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Or, using more compact notation:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;∑ p(c) = 1&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This just says that when we consider &lt;em&gt;all possible outcomes&lt;/em&gt; of the coin flip, one of them must happen. Either it lands heads, or it lands tails—there are no other options.&lt;/p&gt;
&lt;p&gt;This idea of summing over all the states of a variable will show up again later when we start working with multiple variables.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;In summary:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;variable&lt;/strong&gt; represents an uncertain quantity—like the result of a coin flip.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;states&lt;/strong&gt; of that variable are the specific outcomes it can take—&lt;code&gt;heads&lt;/code&gt; or &lt;code&gt;tails&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;domain&lt;/strong&gt; is the full set of those possible states.&lt;/li&gt;
&lt;li&gt;And the &lt;strong&gt;sum of the probabilities of all states&lt;/strong&gt; of the variable must equal 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Interacting Variables: ANDs and ORs&lt;/h2&gt;
&lt;p&gt;Sometimes we’re interested in how two variables—or two events—relate to each other. One of the core rules in probability helps us figure out the chance of &lt;em&gt;either&lt;/em&gt; event happening:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(x or y) = p(x) + p(y) - p(x and y)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This accounts for the fact that if both events can happen together, we shouldn’t double-count that overlap.&lt;/p&gt;
&lt;p&gt;Let’s look at an example with a 6-sided die.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Let &lt;strong&gt;Event A&lt;/strong&gt; be: &lt;em&gt;the die shows an even number&lt;/em&gt; → {2, 4, 6}  
&lt;/li&gt;
&lt;li&gt;Let &lt;strong&gt;Event B&lt;/strong&gt; be: &lt;em&gt;the die shows a number greater than 3&lt;/em&gt; → {4, 5, 6}&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These events &lt;strong&gt;overlap&lt;/strong&gt; at 4 and 6.&lt;/p&gt;
&lt;p&gt;Now let’s calculate the probabilities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;p(A) = 3/6 = 0.5&lt;/strong&gt; (since 2, 4, and 6 are even)  
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;p(B) = 3/6 = 0.5&lt;/strong&gt; (since 4, 5, and 6 are greater than 3)  
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;p(A and B) = 2/6 ≈ 0.333&lt;/strong&gt; (since 4 and 6 satisfy both)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using the formula:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(A or B) = p(A) + p(B) - p(A and B)&lt;/strong&gt;&lt;br /&gt;
&lt;strong&gt;p(A or B) = 0.5 + 0.5 - 0.333 = 0.667&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So there’s about a &lt;strong&gt;66.7% chance&lt;/strong&gt; that the die roll is either even, or greater than 3—or both.&lt;/p&gt;
&lt;p&gt;This rule becomes especially useful when we work with more complex combinations of events later on.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Marginal Probability&lt;/h2&gt;
&lt;p&gt;Now that we've introduced how to handle two variables at once, it might be helpful to see how to take multiple variables and reduce back to one by summing the other variables. This is called calculating the &lt;strong&gt;marginal probability&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;Let’s say we flip two coins. We’ll call them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;c1&lt;/code&gt;: the result of the first flip  
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c2&lt;/code&gt;: the result of the second flip&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each coin has two possible outcomes: &lt;code&gt;heads&lt;/code&gt; or &lt;code&gt;tails&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The full &lt;strong&gt;joint distribution&lt;/strong&gt; (probabilities of combined events) includes all possible combinations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;p(c1 = heads, c2 = heads) = 0.25  
&lt;/li&gt;
&lt;li&gt;p(c1 = heads, c2 = tails) = 0.25  
&lt;/li&gt;
&lt;li&gt;p(c1 = tails, c2 = heads) = 0.25  
&lt;/li&gt;
&lt;li&gt;p(c1 = tails, c2 = tails) = 0.25&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These probabilities all add up to 1, as expected.&lt;/p&gt;
&lt;p&gt;Now let’s say we only care about the result of the &lt;strong&gt;first coin&lt;/strong&gt;. We can compute its &lt;strong&gt;marginal probability&lt;/strong&gt; by summing over all outcomes of the second coin:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(c1 = heads) = p(c1 = heads, c2 = heads) + p(c1 = heads, c2 = tails)&lt;/strong&gt;&lt;br /&gt;
&lt;strong&gt;p(c1 = heads) = 0.25 + 0.25 = 0.5&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(c1 = tails) = p(c1 = tails, c2 = heads) + p(c1 = tails, c2 = tails)&lt;/strong&gt;&lt;br /&gt;
&lt;strong&gt;p(c1 = tails) = 0.25 + 0.25 = 0.5&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This process is called &lt;strong&gt;marginalisation&lt;/strong&gt;. We’re taking a joint distribution of multiple variables and extracting just the part we care about—by summing over the rest.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Conditional Probability&lt;/h2&gt;
&lt;p&gt;Sometimes we don’t just want to know the probability of an event—we want to know the probability &lt;strong&gt;given that something else has already happened&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This brings us to &lt;strong&gt;conditional probability&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We write this as:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(x | y) = p(x and y) / p(y)&lt;/strong&gt;&lt;br /&gt;
or more formally:&lt;br /&gt;
&lt;strong&gt;p(x | y) = p(x, y) / p(y)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This definition tells us how to update our probability of &lt;code&gt;x&lt;/code&gt; when we know that &lt;code&gt;y&lt;/code&gt; is true. The numerator is the joint probability that both &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; happen, and the denominator scales it relative to how likely &lt;code&gt;y&lt;/code&gt; is overall.&lt;/p&gt;
&lt;p&gt;Let’s look at a simple dice example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event A&lt;/strong&gt;: the die shows a 6  
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event B&lt;/strong&gt;: the die shows an even number (2, 4, or 6)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We want to know:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(A | B)&lt;/strong&gt; — the probability that the die shows a 6 &lt;strong&gt;given&lt;/strong&gt; that it shows an even number.&lt;/p&gt;
&lt;p&gt;Using the formula:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(A | B) = p(A and B) / p(B)&lt;/strong&gt;&lt;br /&gt;
&lt;strong&gt;p(A | B) = p(6) / p(even)&lt;/strong&gt;&lt;br /&gt;
&lt;strong&gt;p(A | B) = (1/6) / (3/6) = 1/3&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So if we already know that the die landed on an even number, there’s a &lt;strong&gt;1 in 3&lt;/strong&gt; chance it was a 6.&lt;/p&gt;
&lt;p&gt;This is the essence of conditional probability: we’re updating what we believe about one event in light of new information.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Bayes’ Rule: Deriving It Step by Step&lt;/h2&gt;
&lt;p&gt;Let’s start with something simple but powerful:&lt;/p&gt;
&lt;h3&gt;🔁 Joint probabilities are symmetric&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;joint probability&lt;/strong&gt; of two events &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; is the same regardless of their order:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(x, y) = p(y, x)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This just says: the probability of both &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt; happening is the same as the probability of both &lt;code&gt;y&lt;/code&gt; and &lt;code&gt;x&lt;/code&gt; happening. They describe the same outcome.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;📘 Now recall the definition of conditional probability&lt;/h3&gt;
&lt;p&gt;By definition:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(x | y) = p(x, y) / p(y)&lt;/strong&gt;&lt;br /&gt;
This gives us the probability of &lt;code&gt;x&lt;/code&gt; &lt;em&gt;given that&lt;/em&gt; &lt;code&gt;y&lt;/code&gt; has occurred.&lt;/p&gt;
&lt;p&gt;Likewise:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(y | x) = p(y, x) / p(x)&lt;/strong&gt;&lt;br /&gt;
This gives the probability of &lt;code&gt;y&lt;/code&gt; &lt;em&gt;given that&lt;/em&gt; &lt;code&gt;x&lt;/code&gt; has occurred.&lt;/p&gt;
&lt;p&gt;Let's calculate &lt;code&gt;p(x, y)&lt;/code&gt; now by multiplying both sides by &lt;code&gt;p(y)&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(x,y) = p(x|y) p(y)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Note also that we could have instead done this as:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(y, x) = p(y|x) p(x)&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;🔁 Substituting using the symmetry of joint probabilities&lt;/h3&gt;
&lt;p&gt;We know from earlier that:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(x, y) = p(y, x)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So we can replace &lt;code&gt;p(x, y)&lt;/code&gt; in the first equation with &lt;code&gt;p(y | x) × p(x)&lt;/code&gt; (from the second equation above):&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(x | y) = p(x, y) / p(y)&lt;/strong&gt;&lt;br /&gt;
&lt;strong&gt;p(x | y) = [p(y | x) × p(x)] / p(y)&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;✅ Bayes’ Rule&lt;/h3&gt;
&lt;p&gt;We’ve now arrived at the full expression:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(x | y) = (p(y | x) × p(x)) / p(y)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is &lt;strong&gt;Bayes’ Rule&lt;/strong&gt;. It allows us to reverse the direction of a conditional probability—from &lt;code&gt;p(y | x)&lt;/code&gt; to &lt;code&gt;p(x | y)&lt;/code&gt;— by incorporating:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;p(x)&lt;/strong&gt;: our prior belief about &lt;code&gt;x&lt;/code&gt; (i.e. the probability of x before we have any evidence)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;p(y)&lt;/strong&gt;: the overall likelihood of observing &lt;code&gt;y&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;p(y | x)&lt;/strong&gt;: how likely &lt;code&gt;y&lt;/code&gt; is if &lt;code&gt;x&lt;/code&gt; is true&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Symptoms and Diseases: Using Bayes’ Rule to Flip the Question&lt;/h2&gt;
&lt;p&gt;Imagine you go to the doctor because you have a particular symptom—say, a persistent cough. Naturally, you want to know: &lt;strong&gt;What is the probability that you have a certain disease given this symptom?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This sounds straightforward, but here’s the catch: it’s often &lt;strong&gt;hard to directly measure&lt;/strong&gt; how likely someone with that symptom actually has the disease. Symptoms can be caused by many things, and collecting data on &lt;em&gt;all&lt;/em&gt; people with that symptom and their diagnoses is complicated.&lt;/p&gt;
&lt;p&gt;However, it’s usually &lt;strong&gt;easier to measure two other things&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How often people who &lt;em&gt;have&lt;/em&gt; the disease exhibit that symptom.&lt;/strong&gt; For example, suppose that &lt;strong&gt;80%&lt;/strong&gt; of people with disease X have a cough. This is the probability of the symptom given the disease:  
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;p(cough | disease) = 0.8&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How common the disease is in the general population.&lt;/strong&gt; For example, maybe disease X affects &lt;strong&gt;1 in 1,000&lt;/strong&gt; people:  
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;p(disease) = 0.001&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With those two pieces of information, and using Bayes’ Rule, we can calculate what we really want:&lt;/p&gt;
&lt;h3&gt;What is the probability of having the disease given the symptom?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;p(disease | cough) = [p(cough | disease) × p(disease)] / p(cough)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;But what about &lt;strong&gt;p(cough)&lt;/strong&gt;, the overall probability of coughing? We can estimate that by considering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The chance of coughing &lt;strong&gt;if you have the disease&lt;/strong&gt;: p(cough | disease) = 0.8  
&lt;/li&gt;
&lt;li&gt;The chance of coughing &lt;strong&gt;if you do not have the disease&lt;/strong&gt;: say this is p(cough | no disease) = 0.1 (10% of people cough for other reasons)  
&lt;/li&gt;
&lt;li&gt;The overall chance of having the disease: p(disease) = 0.001  
&lt;/li&gt;
&lt;li&gt;The chance of not having the disease: p(no disease) = 1 - 0.001 = 0.999&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To compute &lt;strong&gt;p(cough)&lt;/strong&gt; — the overall probability that someone has a cough — we can break it down based on whether or not they have the disease. This follows the same logic we saw earlier with &lt;strong&gt;marginal probability&lt;/strong&gt;, where we summed over the possible states of a variable.&lt;/p&gt;
&lt;p&gt;In this case, we’re summing over two mutually exclusive groups: those &lt;strong&gt;with&lt;/strong&gt; the disease and those &lt;strong&gt;without&lt;/strong&gt; it. So we write:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(cough) = p(cough, disease) + p(cough, no disease)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Using the definition of conditional probability  
&lt;/p&gt;
&lt;p&gt;(&lt;strong&gt;p(x, y) = p(x | y) × p(y)&lt;/strong&gt;),  
&lt;/p&gt;
&lt;p&gt;we can rewrite this as:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(cough) = p(cough | disease) × p(disease) + p(cough | no disease) × p(no disease)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is also known as &lt;em&gt;the law of total probability&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Substituting the values:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(cough) = (0.8 × 0.001) + (0.1 × 0.999) = 0.0008 + 0.0999 = 0.1007&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;So even though the symptom is very common (10% of people get it regardless), the contribution from the rare disease (0.08%) is small in comparison.&lt;/p&gt;
&lt;p&gt;Finally, apply Bayes’ Rule:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;p(disease | cough) = (0.8 × 0.001) ÷ 0.1007 = 0.0008 ÷ 0.1007 ≈ 0.00795&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This means:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Given that you have a cough, the chance that you have the disease is about &lt;strong&gt;0.8%&lt;/strong&gt; — or roughly &lt;strong&gt;8 in 1,000&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Even though the disease is quite rare (0.1%), and the symptom common (coughing can be caused by many things), Bayes’ Rule helps us &lt;em&gt;update&lt;/em&gt; the probability of disease &lt;strong&gt;in light of the symptom&lt;/strong&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;Conclusions&lt;/h3&gt;
&lt;p&gt;So what does this have to do with asteroids? I'm glad you asked.&lt;/p&gt;
&lt;p&gt;Throughout this post, we've looked at how probability helps us reason under uncertainty:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We defined &lt;strong&gt;variables&lt;/strong&gt; and their possible &lt;strong&gt;states&lt;/strong&gt;, like coin flips and dice rolls.&lt;/li&gt;
&lt;li&gt;We explored how &lt;strong&gt;probabilities add up&lt;/strong&gt; across all possible outcomes of a variable.&lt;/li&gt;
&lt;li&gt;We looked at &lt;strong&gt;joint probabilities&lt;/strong&gt; and how multiple variables interact.&lt;/li&gt;
&lt;li&gt;We introduced &lt;strong&gt;marginal probability&lt;/strong&gt; by summing over hidden or unobserved variables.&lt;/li&gt;
&lt;li&gt;And we learned how &lt;strong&gt;Bayes’ Rule&lt;/strong&gt; lets us flip the direction of a probability—from something easier to measure (like symptoms given disease) to something we really want to know (like disease given symptoms).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But all of this isn’t just about games of chance.&lt;/p&gt;
&lt;p&gt;An asteroid hitting Earth isn’t like flipping a coin—it’s governed by physics. But we still use probability because we don’t know everything: we might not know the exact location of all near-Earth objects, or the precision of their orbits decades from now.&lt;/p&gt;
&lt;p&gt;In this case, probability reflects &lt;strong&gt;uncertainty in our knowledge&lt;/strong&gt;, not randomness in the universe. And yet, the same mathematical tools apply:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We can define variables like “asteroid hits Earth this year” and assign probabilities to them.&lt;/li&gt;
&lt;li&gt;We can update those probabilities as we gather more evidence—say, spotting a new object in the sky.&lt;/li&gt;
&lt;li&gt;And we can reason about risk using exactly the same logic we used to diagnose a cough.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rules of probability are universal—they help us model and update beliefs whether we're rolling dice, flipping coins, diagnosing disease, or watching the skies.&lt;/p&gt;
</description>
      <pubDate>Fri, 26 Sep 2025 12:00:00 -0600</pubDate>
      <a10:updated>2025-09-26T12:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2642</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/gemma-3-270m-the-little-model-that-cant/</link>
      <category>System.String[]</category>
      <title>Gemma 3 270M, the Little Model that Can't</title>
      <description>&lt;p&gt;Google’s latest addition to the Gemma family is small, tidy, and full of promise: &lt;a href="https://developers.googleblog.com/en/introducing-gemma-3-270m/"&gt;&lt;strong&gt;Gemma 3 270M&lt;/strong&gt;&lt;/a&gt;, a 270-million-parameter model “designed from the ground up for task-specific fine-tuning” and described as a compact foundation for on-device and lightweight deployments.&lt;/p&gt;
&lt;p&gt;Read that press release carefully and it’s easy to see why this launch felt exciting: Google highlights a &lt;strong&gt;256K token vocabulary&lt;/strong&gt; (helpful for rare/specialist terms), strong out-of-the-box instruction following for its size, and production-ready quantization checkpoints so developers can deploy very small, efficient variants.&lt;/p&gt;
&lt;p&gt;They even give a concrete energy claim: an &lt;strong&gt;INT4-quantized&lt;/strong&gt; version “used just 0.75% of the battery for 25 conversations” on a Pixel 9 Pro SoC — a headline-friendly detail that reinforces the efficiency story.&lt;/p&gt;
&lt;p&gt;On &lt;a href="https://huggingface.co/google/gemma-3-270m"&gt;Hugging Face&lt;/a&gt;, the model page doubles down on the same positioning: Gemma 3 270M is part of a family meant to be &lt;em&gt;deployable in limited-resource environments&lt;/em&gt; (laptops, desktops, local cloud instances) and — importantly — the &lt;strong&gt;1B and 270M sizes are listed with a 32K token context window&lt;/strong&gt;. That long context claim is one of the model’s most attention-grabbing specs.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Why the marketing sounds good&lt;/h2&gt;
&lt;p&gt;There’s a clear, defensible niche here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cost and speed&lt;/strong&gt;. Small models are cheaper to train, faster to fine-tune, and can reduce inference costs in production.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Quantization support&lt;/strong&gt;. QAT (quantization-aware training) checkpoints for INT4 are available, which is a real engineering lever to shrink memory and energy use while keeping acceptable quality for many tasks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Large context (on paper)&lt;/strong&gt;. A 32K context window on a 270M-parameter model is unusual and, if it worked perfectly, would be useful for document-level tasks without needing a much bigger model.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So far: plausible use cases include specialized classifiers, extraction pipelines, or fleets of tiny experts tuned for narrow jobs — exactly the scenarios Google highlights.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The downside: real usage (my experience)&lt;/h2&gt;
&lt;p&gt;Theory and PR are one thing; real interactive behavior is another. I tested Gemma 3 270M in a practical setup: I run an “&lt;a href="https://github.com/brucenielson/AIPhilosopher"&gt;AI Karl Popper&lt;/a&gt;” assistant that primes the model with &lt;em&gt;actual&lt;/em&gt; Karl Popper quotations and context, then asks questions such as &lt;strong&gt;“Tell me about induction.”&lt;/strong&gt; With a stronger model I get a rich, Popper-like answer:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;Ah, induction! A persistent phantom in the philosophy of science. Many believe it to be the bedrock of scientific progress, the very engine that drives us from observation to theory. Yet, I find myself, perhaps stubbornly, in profound disagreement.  
&lt;/p&gt;
&lt;p&gt;As I state in &lt;em&gt;Realism and the Aim of Science&lt;/em&gt; (page 188), by induction, one typically means an argument that progresses from empirical premises—singular or particular observations—to universal conclusions, to a universal theory. This progression is thought to occur either with logical certainty or with probability, as calculated by the calculus of probability. However, the argument against induction, as I've often restated, is simple yet devastating: Many theories, like Newton's, which are thought to arise from induction, are demonstrably inconsistent with their alleged inductive premises.&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That’s the kind of grounded, citation-backed, voicey reply I’m aiming for when I feed Popper quotes into the context.&lt;/p&gt;
&lt;p&gt;Now — same pipeline, Gemma 3 270M (out of the box, instruction-tuned checkpoint):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;One way to get around all this is to consider the case under consideration from the perspective of one person rather than several people...  
&lt;/p&gt;
&lt;p&gt;The quote below shows what I think is going on right now....and how much more complex and confusing everything has become. My apologies if they have confused me; otherwise, thank you!!!&lt;br /&gt;
My notes show that there isn't really any specific problem with 'the concept' [sic] - just its formal formulation ...there would still seem to be many...&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That answer fails in basically the only two things that matter! It doesn’t &lt;em&gt;engage the Popper quotations&lt;/em&gt; I provided, and it &lt;strong&gt;doesn’t meaningfully even answer the question about induction&lt;/strong&gt;. In short: Gemma 3 270M, as I used it, ignored the targeted context and produced generic, confused text.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;What this implies (and what the sources actually promise)&lt;/h2&gt;
&lt;p&gt;From the docs and model card you can legitimately take away:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Gemma 3 270M is &lt;strong&gt;designed&lt;/strong&gt; to be a compact, quantizable model for task-specific fine-tuning and efficient deployments. That’s Google’s explicit pitch.&lt;/li&gt;
&lt;li&gt;The model &lt;strong&gt;does&lt;/strong&gt; support long context (32K) in specification and the family’s documentation describes deployability on limited-resource environments such as laptops/desktops. But “spec says 32K” ≠ “in every setup it will reliably use 32K of priming text to answer complex, source-grounded questions.”&lt;/li&gt;
&lt;li&gt;INT4 QAT checkpoints exist, giving engineers a credible path to very low memory/energy deployments — but practical performance and quality tradeoffs (latency, coherence, grounding) depend heavily on the inference stack, quantization details, and any fine-tuning you perform.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, the honest takeaway: Google’s engineering and packaging here are meaningful and exciting in an engineering sense — but that doesn’t automatically make a 270M model an out-of-the-box substitute for a larger LLM in tasks that require faithful use of a long context, precise factual grounding, or nuanced reasoning. Indeed, I have my doubts this is useful at all out of the box unless you just want a stupid but chatty mobile phone bot.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Final verdict&lt;/h2&gt;
&lt;p&gt;I like the direction. Gemma 3 270M is exactly the sort of engineering experiment we need: smaller foundation models that are quantizable, cheap to run, and crafted to be fine-tuned for specific jobs. Google’s blog and the Hugging Face model card show a coherent plan for an ecosystem of small models and the tools to deploy them.&lt;/p&gt;
&lt;p&gt;But in practice — at least in my hands and for my “AI Karl Popper” use case — the model was underwhelming as a drop-in replacement for a larger reasoning/model-with-memory setup. If your use case absolutely depends on reliable use of priming context or high-fidelity reasoning, plan to fine-tune heavily on your domain.&lt;/p&gt;
</description>
      <pubDate>Tue, 16 Sep 2025 09:00:00 -0600</pubDate>
      <a10:updated>2025-09-16T09:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2635</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/keeping-your-ai-chatbot-on-a-leash-tiers-of-agency/</link>
      <category>System.String[]</category>
      <title>Keeping Your AI Chatbot on a Leash: Tiers of Agency</title>
      <description>&lt;h2&gt;Tiered Querying: The Story of How AI Karl Popper Knows When to Ask for Help&lt;/h2&gt;
&lt;p&gt;Imagine AI Karl Popper (or your own AI Agent) sitting at his virtual desk, faced with a user’s question. Does he immediately summon the full power of Gemini and ask it to scour the web? Not quite. Instead, he follows a series of ever‑stronger “escalations,” trying the cheapest, fastest trick first, and only calling in the big guns when simpler steps fail. Here’s the heart of that logic:&lt;/p&gt;
&lt;h3&gt;Tier 1: Raw search results from our Haystack datastore&lt;/h3&gt;
&lt;p&gt;First AI Karl will try to just work with the user's query directly and see if it gives him a good hit.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;retrieved_docs, all_docs = self._doc_pipeline.generate_response(message)
max_score = self.get_max_score(retrieved_docs)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If one of those results earns at least 50 percent confidence, he feels comfortable using them directly. &lt;/p&gt;
&lt;h3&gt;Tier 2: Let Gemini Rewrite the Query&lt;/h3&gt;
&lt;p&gt;But if even the best match scores below 0.50, it’s a sign that the user’s phrasing might be muddy. Rather than immediately plunging into expensive LLM research, Karl slips in a polite request to Gemini: “Could you reword this question for me?” With a cleaner search phrase, he tries once more against the same archive and only adopts the new results if they clearly outperform the first batch.&lt;/p&gt;
&lt;p&gt;For example, suppose someone asks AI Karl Popper something like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;My grandma once went to go see Bozo the clown. He told my grandma that
he believed in Induction. There was Bozo, with his big shoes and red
nose and all of Bozo's friends were getting out of a tiny car. And
then he whispered to my grandma &amp;quot;See, you can tell by the first clown
that another will follow! So induction is correct! Hume was off base!
&lt;em&gt;Honk Honk&lt;/em&gt;&amp;quot; And so my grandma told my father who told my mother who told my hairdresser who told my sister who told me that Bozo was
right! Induction is correct!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There is a legitimate question hidden there among a lot of ridiculous narrative. There is no way the semantic search is going to find much at all based on the user's query in this case. We offer something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# If nothing scores above 0.50, ask the LLM to rewrite a clearer query
if max_score is not None and max_score &amp;lt; 0.50:
    improved_query = self.ask_llm_for_improved_query(message, gemini_chat_history)
    if improved_query:
        new_docs, _ = self._doc_pipeline.generate_response(improved_query)
        new_max = self.get_max_score(new_docs)
        if new_max &amp;gt; max_score + 0.05:
            retrieved_docs, max_score = new_docs, new_max
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Tier 3: Still weak? (max_score &amp;lt; 0.30) Let Gemini itself pick the relevant quotes&lt;/h3&gt;
&lt;p&gt;Next, if the top score still hasn’t broken 0.30, Karl recognizes that neither the original nor the rewritten query is yielding solid matches. He switches tactics—from refining the question to refining the answers themselves. Here he asks Gemini to look at the low‑scoring candidates and pick out the handful of quotes it deems most relevant. If that succeeds, Karl moves forward with those hand‑picked snippets; if not, he applies a simple filter, discarding any quotes below a modest threshold (0.20 if there are at least three solid contenders, or a lower bar of 0.10 otherwise).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if max_score is not None and max_score &amp;lt; 0.30:
    response_text = self.ask_llm_for_quote_relevance(message, retrieved_docs)
    relevant_ids = [int(x) for x in response_text.split(',') if x.strip().isdigit()]
    ranked_docs = [doc for i, doc in enumerate(retrieved_docs, 1) if i in relevant_ids]
else:
    threshold = 0.20 if sum(d.score &amp;gt;= 0.20 for d in retrieved_docs) &amp;gt;= 3 else 0.10
    ranked_docs = [d for d in retrieved_docs if d.score &amp;gt;= threshold]
&lt;/code&gt;&lt;/pre&gt;

&lt;h3&gt;Tier 4: If we still lack at least five strong hits or max_score &amp;lt; 0.60,&lt;/h3&gt;
&lt;p&gt;Finally, even after these steps, there may still not be enough high‑quality material: fewer than five quotes or a top score under 0.60. In that case, Karl waves the white flag on simple retrieval and says, “Alright, Gemini, do your own research.” At this stage, the agent spins up a full ReAct‑style investigation—calling functions, mining external sources (including Wikipedia), and looping until a coherent answer emerges.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#    trigger a full LLM‑powered research sprint
if len(ranked_docs) &amp;lt; 5 or max_score &amp;lt; 0.60:
    research_response, research_docs = self.ask_llm_to_research(message)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By layering these tiers—raw search, query rewriting, quote relevance, and only then full research—AI Karl Popper strikes a balance between efficiency and depth. Most well‑phrased questions end in Tier 1 or Tier 2, saving precious API calls and tokens. Messier queries get gently guided back on track, and only the truly stubborn cases marshal the agent’s full scholarly might. This tiered approach ensures that every question gets the right level of attention, no more and no less.&lt;/p&gt;
</description>
      <pubDate>Tue, 09 Sep 2025 09:00:00 -0600</pubDate>
      <a10:updated>2025-09-09T09:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2634</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/ai-tutorial-a-react-agent-using-gemini-and-haystack/</link>
      <category>System.String[]</category>
      <title>AI Tutorial: A ReAct Agent Using Gemini and Haystack</title>
      <description>&lt;h1&gt;Building a ReAct Research Agent with Gemini and Haystack&lt;/h1&gt;
&lt;p&gt;In &lt;a href="https://www.mindfiretechnology.com/blog/archive/ai-tutorial-function-calling-for-a-react-agent/"&gt;a previous post&lt;/a&gt;, I walked through how function calling can be used to give large language models the ability to reason and act in a loop — using the ReAct pattern pioneered by &lt;a href="https://arxiv.org/abs/2210.03629"&gt;this famous paper&lt;/a&gt;. In that post, I showed a simple ReAct agent that could chain together &amp;quot;thought → action → observation&amp;quot; steps to work through a question, invoking tools that were functions I had built. In that version, I built the ReAct agent from the ground up which meant I had to parse the results to determine which function to call. But then, in this post (i.e. &amp;quot;Using Gemini Function Calling to Build a Research Agent&amp;quot;), I showed how Google's Gemini had function calling like that built-in. This isn't even the best way to do this! I'll have to show you an easier way in a future post.&lt;/p&gt;
&lt;p&gt;In any case, I have released a new version of the Book Search Archive aka &amp;quot;AI Karl Popper&amp;quot; (Mindfire's Open-Source AI software which is really our toy project to work out our open-source stack). It now includes a ReAct based 'research agent' (similar to what we developed in this post: &amp;quot;Using Gemini Function Calling to Build a Research Agent&amp;quot;). I'll explain in a future post why I wanted to do this. In this post, I just want to make people aware of this new version of the code and what has been added.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;ReAct Research Agent&lt;/strong&gt; — designed specifically to allow &amp;quot;AI Karl Popper&amp;quot; (really just the Book Search Archive with a Karl Popper embedded text database attached) to dig in and come up with its own queries to try to answer a user's question using a combination of &lt;strong&gt;document retrieval&lt;/strong&gt;, &lt;strong&gt;function calling&lt;/strong&gt;, and &lt;strong&gt;last-resort fallback to Wikipedia&lt;/strong&gt;. Yes, that's right, AI Karl Popper can now go to Wikipedia to answer questions. Allowing our pseudo-Karl Popper to update this knowledge beyond the books the real Karl Popper wrote in his lifetime. No bad, eh?&lt;/p&gt;
&lt;p&gt;You can &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/b0c6e60610523b42db104c3e16bea361cc0c4a62/react_agent.py"&gt;view the full code here on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What's New in This Agent?&lt;/h2&gt;
&lt;p&gt;This version of the agent is designed to dig into a custom document store (in my case, a PostgreSQL-backed archive powered by the &lt;a href="https://haystack.deepset.ai/"&gt;Haystack framework&lt;/a&gt;) and synthesize answers from the most relevant quotes it finds. Only when that fails — and only after six reasoning steps — does it escalate to a fallback query against Wikipedia. (I say after 6 steps, but honestly, AI Karl tends to 'disobey' and call Wikipedia early. Though he's very apologetic about it. No, I'm not making this up. Try him out!)&lt;/p&gt;
&lt;p&gt;Key improvements include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Tool declarations for document and Wikipedia search:&lt;/strong&gt; Each function is explicitly registered with the Gemini model using Google's function-calling tools. These include &lt;code&gt;search_datastore&lt;/code&gt;, &lt;code&gt;search_wikipedia&lt;/code&gt;, and &lt;code&gt;answer&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Score-aware document formatting:&lt;/strong&gt; The quotes pulled from the document store include not just the content, but also metadata such as confidence scores, sources, and retrieval methods. If AI Karl looks up something on Wikipedia a 'document' is created as a reference and the user is given access to what question led to that Wikipedia look up.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Conversation state and flow control:&lt;/strong&gt; The agent tracks its iteration count, discourages premature Wikipedia access, and gracefully ends when it hits the &lt;code&gt;answer&lt;/code&gt; function.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How It Works at a High Level&lt;/h2&gt;
&lt;p&gt;Here’s a quick summary of the core idea:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Step-by-step reasoning:&lt;/strong&gt; The agent starts by “thinking aloud” — planning how to approach the question.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Search the datastore first:&lt;/strong&gt; Using &lt;code&gt;search_datastore&lt;/code&gt;, it retrieves top-ranked results from the Haystack-powered database and tries to synthesize an answer.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fallback to Wikipedia only if needed:&lt;/strong&gt; If the model still can’t answer the question after several attempts, it uses &lt;code&gt;search_wikipedia&lt;/code&gt; to retrieve page content and try again. Try asking AI Karl when Bozo the clown is born and get ready for him to figure out some way to reasonably answer that question even though there are no references to Bozo the clown in Karl Popper's writings and even though it is ambiguous as to what the user intends. (When the character was created? How old the actor would be today?)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Final response with sources:&lt;/strong&gt; Once it’s ready, the agent calls &lt;code&gt;answer()&lt;/code&gt; to return its final result, listing all information sources used.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here’s an example question you might ask it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;Is induction valid in some cases, particularly when doing statistics?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Or...&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;When was Bozo the Clown born and how old is he today?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The agent might begin by planning its approach, then search the document store for any references to, for example, induction, statistical inference, and related concepts. If it finds quotes supporting the idea that induction is a valid form of reasoning in certain empirical domains, it builds an answer from them. Otherwise, it checks Wikipedia for broader philosophical discussion.&lt;/p&gt;
&lt;h2&gt;Lessons from Gemini's Shifting Tiers&lt;/h2&gt;
&lt;p&gt;As I noted in &lt;a href="https://www.mindfiretechnology.com/blog/archive/google-quietly-ruins-gemini-s-free-tier/"&gt;this other post&lt;/a&gt;, using Gemini effectively now requires a paid API key. Google quietly downgraded the free-tier access earlier this year, making many of these advanced features — especially tool calling — available only via the paid tier. If you're trying to run this agent yourself, make sure to use an authenticated Gemini model (e.g. &lt;code&gt;gemini-1.5-pro&lt;/code&gt; or &lt;code&gt;gemini-1.5-flash&lt;/code&gt;) with API key access.&lt;/p&gt;
&lt;h2&gt;What’s Next?&lt;/h2&gt;
&lt;p&gt;There are several ways I’m looking to improve this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Better fallback reasoning:&lt;/strong&gt; Right now, the Wikipedia fallback is a blunt tool. I’d like to add more nuanced decision-making about when to escalate to external sources. And frankly, it's too slow.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Chain-of-thought memory and reasoning:&lt;/strong&gt; Having the agent remember its past thoughts and decisions more explicitly could help avoid redundant searches.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cross-query synthesis:&lt;/strong&gt; Right now, each search is evaluated in isolation. I'd like to experiment with combining results from multiple queries to build richer answers.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're interested in trying this out, or building your own research agent, the &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/b0c6e60610523b42db104c3e16bea361cc0c4a62/react_agent.py"&gt;GitHub repo&lt;/a&gt; is a great place to start.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Got questions? Want to contribute? Leave a comment or reach out — I’m always happy to talk shop about AI tooling and research agents.&lt;/em&gt;&lt;/p&gt;
</description>
      <pubDate>Fri, 22 Aug 2025 12:00:00 -0600</pubDate>
      <a10:updated>2025-08-22T12:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2633</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/using-gemini-function-calling-to-build-a-research-agent/</link>
      <category>System.String[]</category>
      <title>Using Gemini Function Calling to Build a Research Agent</title>
      <description>&lt;p&gt;Imagine you’ve hired a tiny digital research assistant. You ask it a question, it dives first into your local archive of, in my case, Karl Popper essays, then—if it still thirsts for knowledge—dips into Wikipedia, and finally whispers back its findings. All in under a hundred and fifty lines of code. Let’s eavesdrop on how this “research agent” is built, step by step, and then marvel at the real‑world transcript for our inductive statistics question.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/b0c6e60610523b42db104c3e16bea361cc0c4a62/simple_gemini_function_calling.py"&gt;You can find all my code in my GitHub repository here.&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Rolling Out the Red Carpet&lt;/h3&gt;
&lt;p&gt;You may need to do some installs. Though, if you've been following along with my posts, you've already got all of these:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install google-generativeai wikipedia
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Now for some setup:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import wikipedia
import google.generativeai as genai
from doc_retrieval_pipeline import DocRetrievalPipeline
from haystack import Document
from generator_model import get_secret
from llm_message_utils import send_message

# Gemini credentials, plucked safely from disk
gemini_key = get_secret(r'D:\Documents\Secrets\gemini_secret.txt')
genai.configure(api_key=gemini_key)

# Spin up our Popper archive retriever
doc_retriever = DocRetrievalPipeline(
    table_name=&amp;quot;popper_archive&amp;quot;,
    db_user_name=&amp;quot;postgres&amp;quot;,
    db_password=get_secret(r'D:\Documents\Secrets\postgres_password.txt'),
    postgres_host=&amp;quot;localhost&amp;quot;,
    postgres_port=5432,
    db_name=&amp;quot;postgres&amp;quot;,
    verbose=False,
    llm_top_k=5,
    retriever_top_k_docs=100,
    use_reranker=True,
    embedder_model_name=&amp;quot;BAAI/llm-embedder&amp;quot;,
)

# Start a fresh Gemini chat session
model = genai.GenerativeModel(&amp;quot;gemini-2.0-flash&amp;quot;)
chat  = model.start_chat(history=[])
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here, we gently coax Gemini awake, hand it our Postgres‑backed Karl Popper archive, and let it use our API key. Note that we're here using the DocRetrievalPipeline Haystack pipeline we've previously built. (&lt;a href="https://www.mindfiretechnology.com/blog/archive/giving-your-rag-chatbot-some-agency/"&gt;Last encountered here&lt;/a&gt;.) We're also going to be using our &lt;a href="https://www.mindfiretechnology.com/blog/archive/ai-tutorial-function-calling-for-a-react-agent/"&gt;'one stop shop' Gemini message sender&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;Teaching It to Read&lt;/h3&gt;
&lt;p&gt;Next, we equip our agent with two “tools”: one for your local Haystack document store and one for Wikipedia. But first, a little formatter to turn hay‑stacked metadata into tidy paragraphs.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def format_doc(doc: Document) -&amp;gt; str:
    lines = []
    for k, v in doc.meta.items():
        if not k.startswith(&amp;quot;_&amp;quot;):
            lines.append(f&amp;quot;{k}: {v}&amp;quot;)
    lines.append(doc.content)
    return &amp;quot;\n&amp;quot;.join(lines)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This helper makes Popper’s chapter titles and page numbers look like a friendly librarian’s margin notes, not raw JSON dumps.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def search_datastore(datastore_query: str) -&amp;gt; Dict[str, str]:
    docs, _ = doc_retriever.generate_response(datastore_query, min_score=0.6)
    if not docs:
        return {&amp;quot;result&amp;quot;: &amp;quot;No matching documents found.&amp;quot;}
    snippets = [format_doc(d) for d in docs[:3]]
    return {&amp;quot;result&amp;quot;: &amp;quot;\n\n&amp;quot;.join(snippets)}

def search_wikipedia(wiki_page_search: str, question: str) -&amp;gt; Dict[str, str]:
    try:
        page = wikipedia.page(wiki_page_search, auto_suggest=False)
        return {&amp;quot;result&amp;quot;: page.summary}
    except Exception as e:
        return {&amp;quot;result&amp;quot;: f&amp;quot;Error fetching Wikipedia: {e}&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The first function fetches and formats up to three matches from your Popper archive. The second function slips into Wikipedia and grabs the article summary and returns it.&lt;/p&gt;
&lt;h3&gt;Introducing Our Magic Words&lt;/h3&gt;
&lt;p&gt;Now we register these functions as tools that Gemini can invoke:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from google.generativeai.types import Tool, FunctionDeclaration

tools = [
    Tool(function_declarations=[
        FunctionDeclaration(
            name=&amp;quot;search_datastore&amp;quot;,
            description=&amp;quot;Query the local Popper archive.&amp;quot;,
            parameters={
                &amp;quot;type&amp;quot;: &amp;quot;object&amp;quot;,
                &amp;quot;properties&amp;quot;: {&amp;quot;datastore_query&amp;quot;: {&amp;quot;type&amp;quot;: &amp;quot;string&amp;quot;}},
                &amp;quot;required&amp;quot;: [&amp;quot;datastore_query&amp;quot;],
            },
        ),
        FunctionDeclaration(
            name=&amp;quot;search_wikipedia&amp;quot;,
            description=&amp;quot;Look up a summary on Wikipedia.&amp;quot;,
            parameters={
                &amp;quot;type&amp;quot;: &amp;quot;object&amp;quot;,
                &amp;quot;properties&amp;quot;: {
                    &amp;quot;wiki_page_search&amp;quot;: {&amp;quot;type&amp;quot;: &amp;quot;string&amp;quot;},
                    &amp;quot;question&amp;quot;:         {&amp;quot;type&amp;quot;: &amp;quot;string&amp;quot;},
                },
                &amp;quot;required&amp;quot;: [&amp;quot;wiki_page_search&amp;quot;, &amp;quot;question&amp;quot;],
            },
        ),
    ])
]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This tiny registry is the secret handshake between your Python code and Gemini’s function‑calling machinery.&lt;/p&gt;
&lt;h3&gt;The Heartbeat Loop&lt;/h3&gt;
&lt;p&gt;Here’s the core loop that sends your question, catches any function_calls, executes them, and feeds the results back—until Gemini has no more tools to invoke:&lt;/p&gt;
&lt;p&gt;def run(question: str):
    prompt = (
        &amp;quot;You may call search&lt;em&gt;datastore, then search&lt;/em&gt;wikipedia. &amp;quot;
        &amp;quot;Gather what you need and answer.\n\n&amp;quot;
        f&amp;quot;Question: {question}&amp;quot;
    )
    response = send_message(chat, prompt, tools=tools)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while True:
    candidate = response.candidates[0]
    did_call = False

    for part in candidate.content.parts:
        # 1) Plain text? Print it.
        if getattr(part, &amp;quot;text&amp;quot;, None):
            print(part.text.strip())

        # 2) Function call? Handle it.
        if getattr(part, &amp;quot;function_call&amp;quot;, None):
            did_call = True
            func = part.function_call
            name = func.name
            raw_args = func.args or {}
            pretty_args = {}
            for k, v in raw_args.items():
                if hasattr(v, &amp;quot;WhichOneof&amp;quot;):
                    kind = v.WhichOneof(&amp;quot;kind&amp;quot;)
                    pretty_args[k] = getattr(v, kind)
                else:
                    pretty_args[k] = v

            print(f&amp;quot;\n[Calling {name} with args {pretty_args}]&amp;quot;)
            dispatch = {
                &amp;quot;search_datastore&amp;quot;: search_datastore,
                &amp;quot;search_wikipedia&amp;quot;: search_wikipedia,
            }
            result = dispatch[name](**pretty_args)[&amp;quot;result&amp;quot;]
            print(f&amp;quot;[Result]: {result}\n&amp;quot;)

            # Feed the result back into Gemini
            response = send_message(chat, result, tools=tools)
            break

    if not did_call:
        # No more function calls → done
        break
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Basically we call Gemini and it will give a response. Likely (if things go as intended) it will return with a function call request. We call that function and return the results to Gemini and it will keep calling functions until it's ready to give an answer. Once it stops calling functions it should return a result and that is our researched answer.&lt;/p&gt;
&lt;h3&gt;Showtime: Watching It Think&lt;/h3&gt;
&lt;p&gt;Let’s run it with our burning question:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if __name__ == &amp;quot;__main__&amp;quot;:
    run(&amp;quot;Is induction valid in some cases, particularly when doing statistics?&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The results are pretty amazing for such a small program:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Prompt: You may call search_datastore and then search_wikipedia to gather information. Preferably call both. Then answer the question.

Question: Is induction valid in some cases, particularly when doing statistics?
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The agent first makes a search_datastore call:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Calling search_datastore with args {'datastore_query': 'validity of induction in statistics'}]
[Result]: item_#: 5
page_#: 40
item_id: ch3
file_path: documents\Karl Popper - The Two Fundamental Problems of the Theory of Knowledge-Routledge (2008).epub
book_title: The Two Fundamental Problems of the Theory of Knowledge
paragraph_#: 26
chapter_title: Chapter III THE PROBLEM OF INDUCTION
And yet, all these arguments together still do not suffice to guarantee the validity of the principle of induction.
The inference from our observations as to the validity of the principle of induction in turn represents a generalisation, an inductive inference. And in this generalisation again we (tacitly or explicitly) make an assumption analogous to the earlier assumption formulated in the principle of induction. The only difference is that this time it consists not in the induction of a natural law, but in the induction of a principle of induction.

...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then it calls search_datastore again with a different question:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Calling search_datastore with args {'datastore_query': 'inductive reasoning in statistics'}]
[Result]: page_#: 365
file_path: documents\Realism and the Aim of Science -- Karl Popper -- 2017.pdf
book_title: Realism and the Aim of Science -- Karl Popper -- 2017
section_name: CRITICISM OF PROBABILISTIC INDUCTION
Inductive reasoning is in some respects similar to deductive reasoning, although not quite as conclusive. This situation may be explained by assuming that a conclusive (or an almost conclusive) argument corresponds to the probability 1, an inconclusive argument to a lower probability. Thus inductive reasoning will be part of probability logic; and so will some form or other of the simple inductive rule which is the simplest and most elementary form of all inductive reasoning.    

...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Then it tries search_wikipedia:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Calling search_wikipedia with args {'wiki_page_search': 'Statistical inference', 'question': 'statistical inference'}]
[Result]: Statistical inference is the process of using data analysis to infer properties of an underlying probability distribution. Inferential statistical analysis infers properties of a population, for example by testing hypotheses and deriving estimates. It is assumed that the observed data set is sampled from a larger population.
Inferential statistics can be contrasted with descriptive statistics. Descriptive statistics is solely concerned with properties of the observed data, and it does not rest on the assumption that the data come from a larger population. In machine learning, the term inference is sometimes used instead to mean &amp;quot;make a prediction, by evaluating an already trained model&amp;quot;; in this context inferring properties of the model is referred to as training or learning (rather than inference), and using a model for prediction is referred to as inference (instead of prediction); see also predictive inference.
 Have you called both functions yet? If not, please do so before answering.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;With all that information researched, here is the research agents final response to me:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Response:
Based on the information gathered:

*   **Induction is not deductively valid.** Karl Popper emphasizes that induction is never valid in the sense of deductive reasoning. In cases where all instances have been observed, it essentially becomes deduction.
*   **Induction is used in statistics.** Statistical inference uses data analysis to infer properties of an underlying probability distribution. It infers properties of a population by testing hypotheses and deriving estimates.
*   **Induction is related to probability.** Some sources suggest inductive reasoning can be related to probability logic, where conclusive arguments have a probability of 1, and inconclusive arguments have lower probabilities.
*   **Alternative perspectives exist.** Some argue that inductive reasoning has its own standards and can be considered &amp;quot;inductively valid&amp;quot; if it conforms to those standards. However, this doesn't make it deductively valid.

In summary, while induction is not deductively valid, it is used in statistics to make inferences about populations based on observed data. The validity of induction in statistics relies on probabilistic reasoning and specific inductive standards rather than deductive certainty.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;That's a pretty impressive result!&lt;/p&gt;
&lt;p&gt;This example gives you the basic idea of both how to do function calling for Google's Gemini and also how to create a very basic Large Language Model agent to do research for you. &lt;/p&gt;
</description>
      <pubDate>Fri, 01 Aug 2025 12:00:00 -0600</pubDate>
      <a10:updated>2025-08-01T12:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2632</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/writing-a-robust-do-it-all-gemini-api-caller-in-python/</link>
      <category>System.String[]</category>
      <title>Writing a Robust “Do‑It‑All” Gemini API Caller in Python</title>
      <description>&lt;p&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/google-quietly-ruins-gemini-s-free-tier/"&gt;In our last post&lt;/a&gt;, we talked about how Google killed the rate limits on their free tier. How can we best deal with that problem programmatically? I wrote a utility function that wraps calls to the Gemini API and does all the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Send either a plain “generate content” call or a full chat message  
&lt;/li&gt;
&lt;li&gt;Catch and parse rate‑limit errors (&lt;code&gt;ResourceExhausted&lt;/code&gt;)  
&lt;/li&gt;
&lt;li&gt;Back off for the exact number of seconds Google suggests  
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, even with this functionality, you can get yourself into an infinite loop of retries because Google will lie to you about how long you need to wait before you can try again. (Presumably due to going over your -- now very small -- daily allotment, though I'm somewhat doubtful that they don't just rate limit you before you've reached your daily allotment.)&lt;/p&gt;
&lt;h2&gt;The &amp;quot;send_message&amp;quot; Utility&lt;/h2&gt;
&lt;p&gt;Drop &lt;code&gt;llm_message_utils.py&lt;/code&gt; into your project (from &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/963a15cdd0fb36951e9c05af8e3f4ae2105ee068/llm_message_utils.py"&gt;BookSearchArchive&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from google.api_core.exceptions import ResourceExhausted
from google.generativeai import ChatSession, GenerativeModel
from google.generativeai.types.generation_types import GenerationConfig, GenerateContentResponse
from typing import Any, List, Union
import time
import re

def _extract_retry_seconds(exc: ResourceExhausted, default: int = 15) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;
    Parses &amp;quot;retry_delay { seconds: N }&amp;quot; from exc.details.
    Returns N if found, else returns default.
    &amp;quot;&amp;quot;&amp;quot;
    try:
        details = str(getattr(exc, &amp;quot;details&amp;quot;, &amp;quot;&amp;quot;))
        match = re.search(r'retry_delay\s*{\s*seconds:\s*(\d+)', details)
        if match:
            return int(match.group(1))
    except Exception:
        pass
    return default

def send_message(
    model: Union[ChatSession, GenerativeModel],
    message: str,
    tools: List = None,
    stream: bool = False,
    config: GenerationConfig = None,
    **generation_kwargs: Any
) -&amp;gt; Union[GenerateContentResponse, str]:
    # Build GenerationConfig from kwargs if needed
    if config is None and generation_kwargs:
        config = GenerationConfig(**generation_kwargs)

    try:
        if isinstance(model, ChatSession):
            # Full chat support (streaming, tools)
            return model.send_message(
                message,
                generation_config=config,
                tools=tools,
                stream=stream
            )
        else:
            # Single-shot content generation
            response = model.generate_content(
                contents=message,
                generation_config=config,
                tools=tools,
                stream=stream
            )
            return getattr(response, &amp;quot;text&amp;quot;, None) or &amp;quot;[No response text]&amp;quot;

    except ResourceExhausted as e:
        delay = _extract_retry_seconds(e, default=15)
        print(f&amp;quot;Rate limit hit. Backing off for {delay} seconds…&amp;quot;)
        time.sleep(delay)
        # WARNING: If you’ve hit your daily quota, this will loop forever!
        return send_message(
            model,
            message,
            tools=tools,
            stream=stream,
            config=config,
            **generation_kwargs
        )
&lt;/code&gt;&lt;/pre&gt;

&lt;h2&gt;How it Works&lt;/h2&gt;
&lt;p&gt;Let's start with the _extract_retry_seconds method. This method takes a ResourceExhausted error and gets the &amp;quot;retry_delay&amp;quot; value, which is supposed to be the number of seconds before you can retry again. As mentioned in our last post, this may do you no good if you reached the daily limit.&lt;/p&gt;
&lt;p&gt;The send_message method does the real work. At a minimum, you need to pass a model and a message/prompt.  The 'model' can be either a GenerativeModel or a ChatSession taken from a GenerativeModel. ChatSessions have a lot of extra functionality like tracking history. I try to abstract away some of the pain here by allowing you to pass either. I'm assuming that if you pass a ChatSession you want to talk to the Large Language Model (LLM) in a chat session format, otherwise you just want to generate come content based on the prompt.&lt;/p&gt;
&lt;p&gt;If this is a chat session, I call:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model.send_message(
                    message,
                    generation_config=config,
                    tools=tools,
                    stream=stream
                )
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note that I allow you to pass in configuration arguments, a list of Tools, and a boolean value for streaming or not. &lt;/p&gt;
&lt;p&gt;If you just want to generate content, I instead call:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;response = model.generate_content(
    contents=message,
    generation_config=config,
    tools=tools,
    stream=stream
    )

return getattr(response, &amp;quot;text&amp;quot;, None) or &amp;quot;[No response text]&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Note that if this is a chat session we're returning a GenerateContentResponse so that it comes with the chat history. But if we're generating content, we just return a regular string. &lt;/p&gt;
&lt;p&gt;We wrap all that into a try exception block and trap ResourceExhausted errors. From there we just call the _extract_retry_seconds helper function and then sleep for the number of seconds they told us to. If they didn't give us a number, we default to 15 seconds. &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;except ResourceExhausted as e:
  delay = _extract_retry_seconds(e, default=15)
    print(f&amp;quot;Rate limit hit. Backing off for {delay} seconds…&amp;quot;)
    time.sleep(delay)
    # WARNING: If you’ve hit your daily quota, this will loop forever!
    return send_message(
        model,
        message,
        tools=tools,
        stream=stream,
        config=config,
        **generation_kwargs
      )
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I now can call all LLM calls via this utility function and I'm getting retries for free. We could obviously add a lot more here like exponential backoff or smarter checking for how long to wait.&lt;/p&gt;
&lt;p&gt;We'll be using this little utility in &lt;a href="https://github.com/brucenielson/BookSearchArchive/blob/963a15cdd0fb36951e9c05af8e3f4ae2105ee068/llm_message_utils.py"&gt;BookSearchArchive&lt;/a&gt; for all our calls in preparation for the &lt;a href="https://www.mindfiretechnology.com/blog/archive/ai-tutorial-function-calling-for-a-react-agent/"&gt;ReAct based&lt;/a&gt; research agent we're going to build next to allow our agent to do its own queries to research and find an answer to a user's query.&lt;/p&gt;
</description>
      <pubDate>Thu, 24 Jul 2025 12:00:00 -0600</pubDate>
      <a10:updated>2025-07-24T12:00:00-06:00</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">2631</guid>
      <link>https://www.mindfiretechnology.com/blog/archive/google-quietly-ruins-gemini-s-free-tier/</link>
      <category>System.String[]</category>
      <title>Google Quietly Ruins Gemini’s Free Tier</title>
      <description>&lt;p&gt;Google has quietly chopped the Gemini API free tier down to size—and now it’s about as useful as a screen door on a submarine. Once upon a time, you could fire off &lt;strong&gt;1,500 requests per day&lt;/strong&gt;, but now &lt;strong&gt;Gemini 2.0 Flash&lt;/strong&gt; free tier is capped at &lt;strong&gt;200 requests per day&lt;/strong&gt; (&lt;a href="https://ai.google.dev/gemini-api/docs/rate-limits"&gt;Google Rate Limits&lt;/a&gt; “Free Tier” table; previously 1,500/day according to &lt;a href="https://zapier.com/blog/gemini-api/"&gt;Zapier&lt;/a&gt;: &amp;quot;Free up to 15 requests/min; 1M tokens/min; 1,500 requests/day&amp;quot;) &lt;/p&gt;
&lt;p&gt;Meanwhile, if you switch to &lt;strong&gt;Gemma&lt;/strong&gt;—which on paper allows &lt;strong&gt;30 requests per minute&lt;/strong&gt; and a whopping &lt;strong&gt;14,400 requests per day&lt;/strong&gt; (&lt;a href="https://ai.google.dev/gemini-api/docs/rate-limits"&gt;Google Rate Limits&lt;/a&gt; “Gemma 3 &amp;amp; 3n” row)—you’ll still hit a &lt;code&gt;ResourceExhausted&lt;/code&gt; error long before you approach those limits (&lt;a href="https://support.google.com/gemini/thread/343007251/resource-exhausted-though-to-my-knowledge-staying-within-quotas?hl=en"&gt;Google Support&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I doubt I’m even burning through as many requests as Google claims. For example, my test script throws “ResourceExhausted” after just a handful of calls—despite staying well under the documented RPM and TPM caps. That means prototyping or smoke‑testing your app without buying tokens is now effectively impossible. Want to validate your UX? Better hand over your credit card. Moreover, you'll hit this Gemma limit even if you have a paid tier! &lt;/p&gt;
&lt;p&gt;For anyone chasing budget‑friendly LLM options, it might be time to head back to &lt;a href="https://huggingface.co/"&gt;Hugging Face&lt;/a&gt; or other open‑source alternatives. Because if Google won’t let you play in the sandbox for free, you’ll have to build your sandcastle elsewhere.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Other Related Links:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/78535824/why-does-my-code-send-429-quota-exceeds-on-google-gemini-api-with-generative-ui?utm_source=chatgpt.com"&gt;https://stackoverflow.com/questions/78535824/why-does-my-code-send-429-quota-exceeds-on-google-gemini-api-with-generative-ui?utm_source=chatgpt.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discuss.ai.google.dev/t/gemma-3-27b-rate-limits/73700?utm_source=chatgpt.com"&gt;https://discuss.ai.google.dev/t/gemma-3-27b-rate-limits/73700?utm_source=chatgpt.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mindfiretechnology.com/blog/archive/writing-a-robust-do-it-all-gemini-api-caller-in-python/"&gt;https://www.mindfiretechnology.com/blog/archive/writing-a-robust-do-it-all-gemini-api-caller-in-python/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Thu, 17 Jul 2025 09:00:00 -0600</pubDate>
      <a10:updated>2025-07-17T09:00:00-06:00</a10:updated>
    </item>
  </channel>
</rss>