<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ozgun</title>
    <description>The latest articles on DEV Community by Ozgun (@ozzoo).</description>
    <link>https://dev.to/ozzoo</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3835612%2Fc51b3c55-80b1-4342-bcee-143b8eea534a.jpeg</url>
      <title>DEV Community: Ozgun</title>
      <link>https://dev.to/ozzoo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ozzoo"/>
    <language>en</language>
    <item>
      <title>How I Built a Browser-Native AI Agent Platform with Pyodide (No Backend Required)</title>
      <dc:creator>Ozgun</dc:creator>
      <pubDate>Fri, 20 Mar 2026 14:37:09 +0000</pubDate>
      <link>https://dev.to/ozzoo/how-i-built-a-browser-native-ai-agent-platform-with-pyodide-no-backend-required-1j5d</link>
      <guid>https://dev.to/ozzoo/how-i-built-a-browser-native-ai-agent-platform-with-pyodide-no-backend-required-1j5d</guid>
      <description>&lt;p&gt;I built &lt;a href="https://www.agentop.com" rel="noopener noreferrer"&gt;AgentOp&lt;/a&gt; — a platform where you can&lt;br&gt;
create AI agents and export them as &lt;strong&gt;single standalone HTML files&lt;/strong&gt; that&lt;br&gt;
run entirely in the browser. No server. No Docker. Open the file,&lt;br&gt;
and your Python-powered AI agent is live.&lt;/p&gt;

&lt;p&gt;Here's the technical story of how that actually works.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Most AI agent platforms are server-heavy. You need a backend, a database,&lt;br&gt;
a deployment pipeline. I wanted something different: an agent you could&lt;br&gt;
email to someone as an &lt;code&gt;.html&lt;/code&gt; attachment and it would just &lt;em&gt;work&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The key insight: &lt;strong&gt;Pyodide 0.29.0&lt;/strong&gt; (CPython 3.12 compiled to WebAssembly) lets&lt;br&gt;
you run real Python in the browser. Pair that with LangChain's Python&lt;br&gt;
package and you have a fully capable AI agent runtime with zero backend.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Each generated agent is a self-contained HTML file with three layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Python runtime (Pyodide + LangChain)&lt;/strong&gt;&lt;br&gt;
The agent's tool functions are real Python — loaded into the browser&lt;br&gt;
via Pyodide at runtime. LangChain handles the agent loop, tool calling,&lt;br&gt;
and memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. LLM provider layer (switchable at download time)&lt;/strong&gt;&lt;br&gt;
The user picks their provider when they download the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI / Anthropic&lt;/strong&gt; → direct browser &lt;code&gt;fetch()&lt;/code&gt; calls to the API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local WebLLM&lt;/strong&gt; → &lt;code&gt;@mlc-ai/web-llm&lt;/code&gt; runs a quantized model entirely
in the browser using WebGPU&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. A universal &lt;code&gt;callLLM()&lt;/code&gt; bridge&lt;/strong&gt;&lt;br&gt;
A single JavaScript function handles all three providers, reading&lt;br&gt;
&lt;code&gt;window.PROVIDER&lt;/code&gt; and &lt;code&gt;window.API_KEY&lt;/code&gt; at call time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;callLLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROVIDER&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agentManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;engine&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...],&lt;/span&gt; &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROVIDER&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Anthropic works too — supports direct browser access since Aug 2024&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The HTML Generator
&lt;/h2&gt;

&lt;p&gt;The backend (Django 5.2, Python 3.12) has an &lt;code&gt;AgentHTMLGenerator&lt;/code&gt; class that&lt;br&gt;
assembles the final HTML by injecting components into a Mustache template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentHTMLGenerator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;template_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_get_template_content&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_build_template_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;rendered_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template_content&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rendered_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_inject_encryption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rendered_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;rendered_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_inject_pyodide_auto_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rendered_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# WebLLM path: LangChain.js + Pyodide bridge
&lt;/span&gt;            &lt;span class="n"&gt;rendered_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_inject_webllm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rendered_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;rendered_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_inject_langchain_webllm_infrastructure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rendered_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;rendered_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_inject_runtime_provider_switcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rendered_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rendered_html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important privacy detail&lt;/strong&gt;: agent HTML is generated dynamically at download&lt;br&gt;
time and &lt;strong&gt;never written to disk&lt;/strong&gt;. No agent code is ever stored on the server.&lt;/p&gt;

&lt;p&gt;For the &lt;strong&gt;local WebLLM path&lt;/strong&gt;, it's a hybrid architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LangChain.js&lt;/strong&gt; handles inference (runs JS natively)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pyodide&lt;/strong&gt; handles Python tool execution&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;PyodideToolBridge&lt;/code&gt; passes tool calls back and forth between the
two runtimes via &lt;code&gt;window&lt;/code&gt; globals&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  WebLLM Dual-Mode Function Calling
&lt;/h2&gt;

&lt;p&gt;This was one of the more interesting engineering problems. Not all models&lt;br&gt;
support the same function calling interface, so the platform handles two modes&lt;br&gt;
automatically based on the model selected:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mode 1 — OpenAI API style (Hermes models)&lt;/strong&gt;&lt;br&gt;
Hermes models (e.g. &lt;code&gt;Hermes-2-Pro-Mistral-7B&lt;/code&gt;) support native tool calling&lt;br&gt;
via LangChain.js. The flow is clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Input → WebLLMAgentManager → LangChain.js Agent → ChatWebLLM
                                          ↓
                                    Tool Call (Native)
                                          ↓
                                  PyodideToolBridge (JS↔Python)
                                          ↓
                                    Python Tools (Pyodide)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mode 2 — Manual parsing (Llama 3.1 and others)&lt;/strong&gt;&lt;br&gt;
Llama 3.1 doesn't speak the OpenAI tool format, so the platform injects a&lt;br&gt;
custom system prompt and parses &lt;code&gt;&amp;lt;function&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;tool_call&amp;gt;&lt;/code&gt; XML tags from&lt;br&gt;
the raw model output. It's single-shot — the model must emit a valid tool&lt;br&gt;
call on its first response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Input → ManualFunctionCallingAgent → Raw WebLLM Engine
                                          ↓
                             Custom System Prompt + Raw Response
                                          ↓
                             Parse &amp;lt;function&amp;gt; / &amp;lt;tool_call&amp;gt; Tags
                                          ↓
                                  PyodideToolBridge (JS↔Python)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mode is detected automatically from &lt;code&gt;function_calling_method&lt;/code&gt; in the model&lt;br&gt;
registry (&lt;code&gt;webllm_models.py&lt;/code&gt;). Agent authors don't have to think about it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Package Management Inside the Browser
&lt;/h2&gt;

&lt;p&gt;This was the trickiest part. Pyodide has its own package ecosystem.&lt;br&gt;
For cloud providers (OpenAI/Anthropic), the agent needs &lt;code&gt;langchain_openai&lt;/code&gt;&lt;br&gt;
or &lt;code&gt;langchain_anthropic&lt;/code&gt; — but these have to be installed at runtime via&lt;br&gt;
&lt;code&gt;micropip&lt;/code&gt; inside the browser.&lt;/p&gt;

&lt;p&gt;The generator handles this automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_filter_packages_by_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;merged_packages&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# No Python LangChain needed — using LangChain.js instead
&lt;/span&gt;        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builtins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filtered_pypi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;langchain_openai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0.a3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;custom_wheels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.agentop.com/static/packages/uuid_utils-...whl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builtins&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;user_pypi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;custom_wheels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes — some packages needed a custom Pyodide-compatible &lt;code&gt;.whl&lt;/code&gt; wheel&lt;br&gt;
(compiled for &lt;code&gt;wasm32&lt;/code&gt;). &lt;code&gt;uuid_utils&lt;/code&gt; is one example. Building these&lt;br&gt;
took a fair amount of Emscripten time.&lt;/p&gt;


&lt;h2&gt;
  
  
  API Key Security
&lt;/h2&gt;

&lt;p&gt;A standalone HTML file can't have a server to protect secrets. The&lt;br&gt;
solution: &lt;strong&gt;client-side AES-256-GCM encryption&lt;/strong&gt; via the Web Crypto API.&lt;br&gt;
The user sets a master password once. Key derivation uses&lt;br&gt;
&lt;strong&gt;PBKDF2 (100,000 iterations)&lt;/strong&gt; — the ciphertext is what gets embedded&lt;br&gt;
in the downloaded file, not the raw key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User enters master password → PBKDF2 key derivation → AES-256-GCM decryption → window.API_KEY populated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The encryption/decryption logic is inlined into the HTML on generation,&lt;br&gt;
so the file works completely offline. Local WebLLM agents skip this entirely —&lt;br&gt;
no API key needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Didn't Work
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;gRPC in the browser&lt;/strong&gt;: I tried adding Gemini as a provider via
LangChain's Google integration. It uses gRPC under the hood — which
doesn't work in browser environments at all. Had to skip it for now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not all Python packages compile to WASM&lt;/strong&gt;: If your agent needs
something like &lt;code&gt;numpy&lt;/code&gt; for heavy computation, you're usually fine (it's
in Pyodide's standard set). But niche packages with C extensions are
often missing. You have to compile them with Emscripten yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebGPU availability&lt;/strong&gt;: Local WebLLM requires WebGPU. It works great
in Chrome/Chromium (tested on desktop and even Steam Deck), but Firefox
support is still limited.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;A full marketplace (Django 5.2 + PostgreSQL) where you can browse, rate,&lt;br&gt;
fork, and collect agents — then download any of them as a single &lt;code&gt;.html&lt;/code&gt; file&lt;br&gt;
that runs entirely client-side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runs Python in the browser via Pyodide 0.29.0 (Python 3.12)&lt;/li&gt;
&lt;li&gt;Calls OpenAI/Anthropic APIs directly from JS&lt;/li&gt;
&lt;li&gt;Or runs a local LLM entirely offline with WebLLM + WebGPU&lt;/li&gt;
&lt;li&gt;Encrypts your API key with AES-256-GCM + PBKDF2 — never touches the server&lt;/li&gt;
&lt;li&gt;Works from &lt;code&gt;file://&lt;/code&gt; — no server needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(&lt;a href="https://github.com/ozgunay/agentop" rel="noopener noreferrer"&gt;https://github.com/ozgunay/agentop&lt;/a&gt;)&lt;br&gt;
Try it live: &lt;a href="https://www.agentop.com" rel="noopener noreferrer"&gt;agentop.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with: Django 5.2, Python 3.12, PostgreSQL 15, Pyodide 0.29.0,&lt;br&gt;
LangChain, WebLLM (@mlc-ai/web-llm), LangChain.js, Alpine.js, HTMX,&lt;br&gt;
Mustache (chevron)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>webassembly</category>
      <category>ai</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
