<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: val-town</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/val-town.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2025-08-06T16:37:13+00:00</updated><author><name>Simon Willison</name></author><entry><title>Tom MacWright: Observable Notebooks 2.0</title><link href="https://simonwillison.net/2025/Aug/6/observable-notebooks-20/#atom-tag" rel="alternate"/><published>2025-08-06T16:37:13+00:00</published><updated>2025-08-06T16:37:13+00:00</updated><id>https://simonwillison.net/2025/Aug/6/observable-notebooks-20/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://macwright.com/2025/07/31/observable-notebooks-2"&gt;Tom MacWright: Observable Notebooks 2.0&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Observable announced &lt;a href="https://observablehq.com/notebook-kit/"&gt;Observable Notebooks 2.0&lt;/a&gt; last week - the latest take on their JavaScript notebook technology, this time with an &lt;a href="https://observablehq.com/notebook-kit/kit"&gt;open file format&lt;/a&gt; and a brand new &lt;a href="https://observablehq.com/notebook-kit/desktop"&gt;macOS desktop app&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Tom MacWright worked at Observable during their first iteration and here provides thoughtful commentary from an insider-to-outsider perspective on how their platform has evolved over time.&lt;/p&gt;
&lt;p&gt;I particularly appreciated this aside on the downsides of evolving your own not-quite-standard language syntax:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Notebook Kit and Desktop &lt;a href="https://observablehq.com/notebook-kit/#vanilla-java-script"&gt;support vanilla JavaScript&lt;/a&gt;, which is excellent and cool. The Observable changes to JavaScript were always tricky and meant that we struggled to use off-the-shelf parsers, and users couldn't use standard JavaScript tooling like eslint. This is stuff like the &lt;code&gt;viewof&lt;/code&gt; operator which meant that &lt;a href="https://observablehq.com/@observablehq/observable-javascript"&gt;Observable was not JavaScript&lt;/a&gt;. [...] &lt;em&gt;Sidenote&lt;/em&gt;: I now work on &lt;a href="https://www.val.town/"&gt;Val Town&lt;/a&gt;, which is also a platform based on writing JavaScript, and when I joined it &lt;em&gt;also&lt;/em&gt; had a tweaked version of JavaScript. We used the &lt;code&gt;@&lt;/code&gt; character to let you 'mention' other vals and implicitly import them. This was, like it was in Observable, not worth it and we switched to standard syntax: don't mess with language standards folks!&lt;/p&gt;
&lt;/blockquote&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tom-macwright"&gt;tom-macwright&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/val-town"&gt;val-town&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="observable"/><category term="tom-macwright"/><category term="val-town"/></entry><entry><title>Stevens: a hackable AI assistant using a single SQLite table and a handful of cron jobs</title><link href="https://simonwillison.net/2025/Apr/13/stevens/#atom-tag" rel="alternate"/><published>2025-04-13T20:58:09+00:00</published><updated>2025-04-13T20:58:09+00:00</updated><id>https://simonwillison.net/2025/Apr/13/stevens/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.geoffreylitt.com/2025/04/12/how-i-made-a-useful-ai-assistant-with-one-sqlite-table-and-a-handful-of-cron-jobs"&gt;Stevens: a hackable AI assistant using a single SQLite table and a handful of cron jobs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Geoffrey Litt reports on Stevens, a shared digital assistant he put together for his family using SQLite and scheduled tasks running on Val Town.&lt;/p&gt;
&lt;p&gt;The design is refreshingly simple considering how much it can do. Everything works around a single &lt;code&gt;memories&lt;/code&gt; table. A memory has text, tags, creation metadata and an optional &lt;code&gt;date&lt;/code&gt; for things like calendar entries and weather reports.&lt;/p&gt;
&lt;p&gt;Everything else is handled by scheduled jobs to popular weather information and events from Google Calendar, a Telegram integration offering a chat UI and a neat system where USPS postal email delivery notifications are run through Val's own email handling mechanism to trigger a Claude prompt to add those as memories too. &lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://www.val.town/x/geoffreylitt/stevensDemo"&gt;the full code on Val Town&lt;/a&gt;, including &lt;a href="https://www.val.town/x/geoffreylitt/stevensDemo/code/dailyBriefing/sendDailyBrief.ts"&gt;the daily briefing prompt&lt;/a&gt; that incorporates most of the personality of the bot.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geoffrey-litt"&gt;geoffrey-litt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/val-town"&gt;val-town&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-personality"&gt;ai-personality&lt;/a&gt;&lt;/p&gt;



</summary><category term="sqlite"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="geoffrey-litt"/><category term="val-town"/><category term="ai-personality"/></entry><entry><title>What we learned copying all the best code assistants</title><link href="https://simonwillison.net/2025/Jan/4/what-we-learned-copying-all-the-best-code-assistants/#atom-tag" rel="alternate"/><published>2025-01-04T20:49:29+00:00</published><updated>2025-01-04T20:49:29+00:00</updated><id>https://simonwillison.net/2025/Jan/4/what-we-learned-copying-all-the-best-code-assistants/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.val.town/blog/fast-follow/"&gt;What we learned copying all the best code assistants&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Steve Krouse describes Val Town's experience so far building features that use LLMs, starting with completions (powered by &lt;a href="https://codeium.com/"&gt;Codeium&lt;/a&gt; and Val Town's own &lt;a href="https://github.com/val-town/codemirror-codeium"&gt;codemirror-codeium&lt;/a&gt; extension) and then rolling through several versions of their &lt;a href="https://www.val.town/townie"&gt;Townie&lt;/a&gt; code assistant, initially powered by GPT 3.5 but later upgraded to Claude 3.5 Sonnet.&lt;/p&gt;
&lt;p&gt;This is a really interesting space to explore right now because there is so much activity in it from larger players. Steve classifies Val Town's approach as "fast following" - trying to spot the patterns that are proven to work and bring them into their own product.&lt;/p&gt;
&lt;p&gt;It's challenging from a strategic point of view because Val Town's core differentiator isn't meant to be AI coding assistance: they're trying to build the best possible ecosystem for hosting and iterating lightweight server-side JavaScript applications. Isn't this stuff all a distraction from that larger goal?&lt;/p&gt;
&lt;p&gt;Steve concludes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;However, it still feels like there’s a lot to be gained with a fully-integrated web AI code editor experience in Val Town – even if we can only get 80% of the features that the big dogs have, and a couple months later. It doesn’t take that much work to copy the best features we see in other tools. The benefits to a fully integrated experience seems well worth that cost. In short, we’ve had a lot of success fast-following so far, and think it’s worth continuing to do so.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It continues to be wild to me how features like this are easy enough to build now that they can be part-time side features at a small startup, and not the entire project.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=42586042"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-engineering"&gt;prompt-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/val-town"&gt;val-town&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/steve-krouse"&gt;steve-krouse&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai"/><category term="prompt-engineering"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="val-town"/><category term="steve-krouse"/></entry><entry><title>Cerebras Coder</title><link href="https://simonwillison.net/2024/Oct/31/cerebras-coder/#atom-tag" rel="alternate"/><published>2024-10-31T22:39:15+00:00</published><updated>2024-10-31T22:39:15+00:00</updated><id>https://simonwillison.net/2024/Oct/31/cerebras-coder/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.val.town/v/stevekrouse/cerebras_coder"&gt;Cerebras Coder&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Val Town founder Steve Krouse has been building demos on top of the Cerebras API that runs Llama3.1-70b at 2,000 tokens/second.&lt;/p&gt;
&lt;p&gt;Having a capable LLM with that kind of performance turns out to be really interesting. Cerebras Coder is a demo that implements Claude Artifact-style on-demand JavaScript apps, and having it run at that speed means changes you request are visible within less than a second:&lt;/p&gt;
&lt;div style="max-width: 100%;"&gt;
    &lt;video 
        controls 
        preload="none"
        poster="https://static.simonwillison.net/static/2024/cascade-emoji.jpeg"
        style="width: 100%; height: auto;"&gt;
        &lt;source src="https://static.simonwillison.net/static/2024/cascade-emoji.mp4" type="video/mp4"&gt;
    &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;Steve's implementation (created with the help of &lt;a href="https://www.val.town/townie"&gt;Townie&lt;/a&gt;, the Val Town code assistant) demonstrates the simplest possible version of an iframe sandbox:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;iframe
    srcDoc={code}
    sandbox="allow-scripts allow-modals allow-forms allow-popups allow-same-origin allow-top-navigation allow-downloads allow-presentation allow-pointer-lock"
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;code&lt;/code&gt; is populated by a &lt;code&gt;setCode(...)&lt;/code&gt; call inside a React component.&lt;/p&gt;
&lt;p&gt;The most interesting applications of LLMs continue to be where they operate in a tight loop with a human - this can make those review loops potentially much faster and more productive.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/stevekrouse/status/1851995718514327848"&gt;@stevekrouse&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/iframes"&gt;iframes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sandboxing"&gt;sandboxing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/react"&gt;react&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llama"&gt;llama&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/val-town"&gt;val-town&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/steve-krouse"&gt;steve-krouse&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/cerebras"&gt;cerebras&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-performance"&gt;llm-performance&lt;/a&gt;&lt;/p&gt;



</summary><category term="iframes"/><category term="sandboxing"/><category term="ai"/><category term="react"/><category term="generative-ai"/><category term="llama"/><category term="llms"/><category term="ai-assisted-programming"/><category term="val-town"/><category term="steve-krouse"/><category term="cerebras"/><category term="llm-performance"/></entry><entry><title>System prompt for val.town/townie</title><link href="https://simonwillison.net/2024/Aug/28/system-prompt-for-townie/#atom-tag" rel="alternate"/><published>2024-08-28T03:33:11+00:00</published><updated>2024-08-28T03:33:11+00:00</updated><id>https://simonwillison.net/2024/Aug/28/system-prompt-for-townie/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://gist.github.com/simonw/d8cc934ad76b3bba82127937d45dc719"&gt;System prompt for val.town/townie&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;a href="https://www.val.town/"&gt;Val Town&lt;/a&gt; (&lt;a href="https://simonwillison.net/2024/Jun/21/search-based-rag/"&gt;previously&lt;/a&gt;) provides hosting and a web-based coding environment for Vals - snippets of JavaScript/TypeScript that can run server-side as scripts, on a schedule or hosting a web service.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.val.town/townie"&gt;Townie&lt;/a&gt; is Val's new AI bot, providing a conversational chat interface for creating fullstack web apps (with blob or SQLite persistence) as Vals.&lt;/p&gt;
&lt;p&gt;In the &lt;a href="https://twitter.com/stevekrouse/status/1828454235756798287"&gt;most recent release&lt;/a&gt; of Townie Val added the ability to inspect and edit its system prompt!&lt;/p&gt;
&lt;p&gt;I've archived a copy &lt;a href="https://gist.github.com/simonw/d8cc934ad76b3bba82127937d45dc719"&gt;in this Gist&lt;/a&gt;, as a snapshot of how Townie works today. It's surprisingly short, relying heavily on the model's existing knowledge of Deno and TypeScript.&lt;/p&gt;
&lt;p&gt;I enjoyed the use of "tastefully" in this bit:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Tastefully add a view source link back to the user's val if there's a natural spot for it and it fits in the context of what they're building. You can generate the val source url via import.meta.url.replace("esm.town", "val.town").&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The prompt includes a few code samples, like this one demonstrating how to use Val's SQLite package:&lt;/p&gt;
&lt;div class="highlight highlight-source-ts"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-s1"&gt;sqlite&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;"https://esm.town/v/stevekrouse/sqlite"&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-smi"&gt;KEY&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-smi"&gt;URL&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;meta&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;pathname&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;split&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"/"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;at&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;-&lt;/span&gt;&lt;span class="pl-c1"&gt;1&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;sqlite&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;execute&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;`select * from &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-smi"&gt;KEY&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;_users where id = ?`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;1&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;rows&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;id&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It also reveals the existence of Val's very own delightfully simple &lt;a href="https://www.val.town/v/maxm/imggenurl"&gt;image generation endpoint Val&lt;/a&gt;, currently powered by &lt;a href="https://fal.ai/models/fal-ai/fast-lightning-sdxl"&gt;Stable Diffusion XL Lightning on fal.ai&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;If you want an AI generated image, use https://maxm-imggenurl.web.val.run/the-description-of-your-image to dynamically generate one.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://maxm-imggenurl.web.val.run/a%20fun%20colorful%20raccoon%20with%20a%20wildly%20inapropriate%20hat"&gt;a fun colorful raccoon with a wildly inappropriate hat&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Val are also running their own &lt;a href="https://www.val.town/v/std/openaiproxy"&gt;gpt-4o-mini proxy&lt;/a&gt;, free to users of their platform:&lt;/p&gt;
&lt;div class="highlight highlight-source-ts"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-smi"&gt;OpenAI&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;"https://esm.town/v/std/openai"&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;openai&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-smi"&gt;OpenAI&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;completion&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;openai&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;chat&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;completions&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;create&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c1"&gt;messages&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
    &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;role&lt;/span&gt;: &lt;span class="pl-s"&gt;"user"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-s"&gt;"Say hello in a creative way"&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;model&lt;/span&gt;: &lt;span class="pl-s"&gt;"gpt-4o-mini"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;max_tokens&lt;/span&gt;: &lt;span class="pl-c1"&gt;30&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Val developer JP Posma wrote a lot more about Townie in &lt;a href="https://blog.val.town/blog/codegen/"&gt;How we built Townie – an app that generates fullstack apps&lt;/a&gt;, describing their prototyping process and revealing that the current model it's using is Claude 3.5 Sonnet.&lt;/p&gt;
&lt;p&gt;Their current system prompt was refined over many different versions - initially they were including 50 example Vals at quite a high token cost, but they were able to reduce that down to the linked system prompt which includes condensed documentation and just one templated example.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/stevekrouse/status/1828454235756798287"&gt;@stevekrouse&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/typescript"&gt;typescript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/deno"&gt;deno&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-engineering"&gt;prompt-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/val-town"&gt;val-town&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-3-5-sonnet"&gt;claude-3-5-sonnet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/text-to-image"&gt;text-to-image&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/system-prompts"&gt;system-prompts&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="sqlite"/><category term="ai"/><category term="typescript"/><category term="deno"/><category term="prompt-engineering"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="anthropic"/><category term="claude"/><category term="val-town"/><category term="claude-3-5-sonnet"/><category term="text-to-image"/><category term="system-prompts"/></entry><entry><title>Building search-based RAG using Claude, Datasette and Val Town</title><link href="https://simonwillison.net/2024/Jun/21/search-based-rag/#atom-tag" rel="alternate"/><published>2024-06-21T20:44:24+00:00</published><updated>2024-06-21T20:44:24+00:00</updated><id>https://simonwillison.net/2024/Jun/21/search-based-rag/#atom-tag</id><summary type="html">
    &lt;p&gt;Retrieval Augmented Generation (RAG) is a technique for adding extra "knowledge" to systems built on LLMs, allowing them to answer questions against custom information not included in their training data. A common way to implement this is to take a question from a user, translate that into a set of search queries, run those against a search engine and then feed the results back into the LLM to generate an answer.&lt;/p&gt;
&lt;p&gt;I built a basic version of this pattern against the brand new &lt;a href="https://simonwillison.net/2024/Jun/20/claude-35-sonnet/"&gt;Claude 3.5 Sonnet&lt;/a&gt; language model, using &lt;a href="https://www.sqlite.org/fts5.html"&gt;SQLite full-text search&lt;/a&gt; running in &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; as the search backend and &lt;a href="https://www.val.town/"&gt;Val Town&lt;/a&gt; as the prototyping platform.&lt;/p&gt;
&lt;p&gt;The implementation took just over an hour, during a live coding session with Val.Town founder Steve Krouse. I was the latest guest on Steve's &lt;a href="https://www.youtube.com/@ValDotTown/videos?view=2&amp;amp;sort=dd&amp;amp;live_view=503&amp;amp;shelf_id=2"&gt;live streaming series&lt;/a&gt; where he invites people to hack on projects with his help.&lt;/p&gt;
&lt;p&gt;You can watch the video below or &lt;a href="https://www.youtube.com/watch?v=9pmC3P1fUFo"&gt;on YouTube&lt;/a&gt;. Here are my own detailed notes to accompany the session.&lt;/p&gt;
&lt;iframe style="max-width: 100%" width="560" height="315" src="https://www.youtube-nocookie.com/embed/9pmC3P1fUFo" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen="allowfullscreen"&gt; &lt;/iframe&gt;
&lt;h4 id="claude-artifacts-demo"&gt;Bonus: Claude 3.5 Sonnet artifacts demo&lt;/h4&gt;
&lt;p&gt;We started the stream by chatting a bit about the new Claude 3.5 Sonnet release. This turned into an unplanned demo of their "artifacts" feature where Claude can now build you an interactive web page on-demand.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/claude-rag/frame_000350.jpg" alt="Screenshot of the Claude AI interface showing an interactive Mandelbrot fractal explorer and the prompts used to create it" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;At &lt;a href="https://www.youtube.com/watch?v=9pmC3P1fUFo&amp;amp;t=3m02s"&gt;3m02s&lt;/a&gt; I prompted it with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Build me a web app that teaches me about mandelbrot fractals, with interactive widgets&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This worked! Here's &lt;a href="https://gist.github.com/simonw/e57932549e47db2e45f1f75742b078f1"&gt;the code it wrote&lt;/a&gt; - I haven't yet found a good path for turning that into a self-hosted interactive page yet.&lt;/p&gt;
&lt;p&gt;This didn't support panning, so I added:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Again but let me drag on the canvas element to pan around&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Which &lt;a href="https://gist.github.com/simonw/76ef926312093333b48093da6def59fc"&gt;gave me this&lt;/a&gt;. Pretty impressive!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/claude-rag/mandelbrot.gif" alt="Animated demo of Mandelbrot Fractor Explorer - I can slide the zoom and max iterations sliders and pan around by dragging my mouse on the canvas" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="ingredients-for-rag"&gt;Ingredients for the RAG project&lt;/h4&gt;
&lt;p&gt;RAG is often implemented using &lt;a href="https://simonwillison.net/2023/Oct/23/embeddings/#answering-questions-with-retrieval-augmented-generation"&gt;vector search against embeddings&lt;/a&gt;, but there's an alternative approach where you turn the user's question into some full-text search queries, run those against a traditional search engine, then feed the results back into an LLM and ask it to use them to answer the question.&lt;/p&gt;
&lt;p&gt;SQLite includes &lt;a href="https://www.sqlite.org/fts5.html"&gt;surprisingly good full-text search&lt;/a&gt;, and I've built a lot of tools against that in the past - including &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#configuring-full-text-search"&gt;sqlite-utils enable-fts&lt;/a&gt; and &lt;a href="https://docs.datasette.io/en/latest/full_text_search.html"&gt;Datasette's FTS features&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My blog has a lot of content, which lives in a Django PostgreSQL database. But I also have a GitHub Actions repository which &lt;a href="https://github.com/simonw/simonwillisonblog-backup/blob/main/.github/workflows/backup.yml"&gt;backs up that data&lt;/a&gt; as JSON, and then publishes a SQLite copy of it to &lt;a href="https://datasette.simonwillison.net/"&gt;datasette.simonwillison.net&lt;/a&gt; - which means I have a Datasette-powered JSON API for running searches against my content.&lt;/p&gt;
&lt;p&gt;Let's use that API to build a question answering RAG system!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/claude-rag/frame_002158.jpg" alt="Screenshot of Datasette interface running a search with a custom SQL query for ruby on rails" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Step one then was to prototype up a SQL query we could use with that API to get back search results. After some iteration I got to this:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt;
  &lt;span class="pl-c1"&gt;blog_entry&lt;/span&gt;.&lt;span class="pl-c1"&gt;id&lt;/span&gt;,
  &lt;span class="pl-c1"&gt;blog_entry&lt;/span&gt;.&lt;span class="pl-c1"&gt;title&lt;/span&gt;,
  &lt;span class="pl-c1"&gt;blog_entry&lt;/span&gt;.&lt;span class="pl-c1"&gt;body&lt;/span&gt;,
  &lt;span class="pl-c1"&gt;blog_entry&lt;/span&gt;.&lt;span class="pl-c1"&gt;created&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt;
  blog_entry
  &lt;span class="pl-k"&gt;join&lt;/span&gt; blog_entry_fts &lt;span class="pl-k"&gt;on&lt;/span&gt; &lt;span class="pl-c1"&gt;blog_entry_fts&lt;/span&gt;.&lt;span class="pl-c1"&gt;rowid&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;blog_entry&lt;/span&gt;.&lt;span class="pl-c1"&gt;rowid&lt;/span&gt;
&lt;span class="pl-k"&gt;where&lt;/span&gt;
  blog_entry_fts match :search
&lt;span class="pl-k"&gt;order by&lt;/span&gt;
  rank
&lt;span class="pl-k"&gt;limit&lt;/span&gt;
  &lt;span class="pl-c1"&gt;10&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href="https://datasette.simonwillison.net/simonwillisonblog?sql=select%0D%0A++blog_entry.id%2C%0D%0A++blog_entry.title%2C%0D%0A++blog_entry.body%2C%0D%0A++blog_entry.created%0D%0Afrom%0D%0A++blog_entry%0D%0A++join+blog_entry_fts+on+blog_entry_fts.rowid+%3D+blog_entry.rowid%0D%0Awhere%0D%0A++blog_entry_fts+match+%3Asearch%0D%0Aorder+by%0D%0A++rank%0D%0Alimit%0D%0A++10&amp;amp;search=%22shot-scraper%22+OR+%22screenshot%22+OR+%22web%22+OR+%22tool%22+OR+%22automation%22+OR+%22CLI%22"&gt;Try that here&lt;/a&gt;. The query works by joining the &lt;code&gt;blog_entry&lt;/code&gt; table to the SQLite FTS &lt;code&gt;blog_entry_fts&lt;/code&gt; virtual table, matched against the &lt;code&gt;?search=&lt;/code&gt; parameter from the URL.&lt;/p&gt;
&lt;p&gt;When you join against a FTS table like this a &lt;code&gt;rank&lt;/code&gt; column is exposed with the relevance score for each match.&lt;/p&gt;
&lt;p&gt;Adding &lt;code&gt;.json&lt;/code&gt; to the above URL turns it into an API call... so now we have a search API we can call from other code.&lt;/p&gt;
&lt;h4 id="building-it"&gt;A plan for the build&lt;/h4&gt;
&lt;p&gt;We spent the rest of the session writing code in Val Town, which offers a browser editor for a server-side Deno-based environment for executing JavaScript (and TypeScript) code.&lt;/p&gt;
&lt;p&gt;The finished code does the following:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Accepts a user's question from the &lt;code&gt;?question=&lt;/code&gt; query string.&lt;/li&gt;
&lt;li&gt;Asks Claude 3.5 Sonnet to turn that question into multiple single-word search queries, using a Claude function call to enforce a schema of a JSON list of strings.&lt;/li&gt;
&lt;li&gt;Turns that list of keywords into a SQLite FTS query that looks like this: &lt;code&gt;"shot-scraper" OR "screenshot" OR "web" OR "tool" OR "automation" OR "CLI"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Runs that query against Datasette to get back the top 10 results.&lt;/li&gt;
&lt;li&gt;Combines the title and body from each of those results into a longer context.&lt;/li&gt;
&lt;li&gt;Calls Claude 3 again (originally Haiku, but then we upgraded to 3.5 Sonnet towards the end) with that context and ask it to answer the question.&lt;/li&gt;
&lt;li&gt;Return the results to the user.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id="annotated-final-script"&gt;The annotated final script&lt;/h4&gt;
&lt;p&gt;Here's the final script we ended up with, with inline commentary. Here's the initial setup:&lt;/p&gt;
&lt;div class="highlight highlight-source-ts"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-smi"&gt;Anthropic&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;"npm:@anthropic-ai/sdk@0.24.0"&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-c"&gt;/* This automatically picks up the API key from the ANTHROPIC_API_KEY&lt;/span&gt;
&lt;span class="pl-c"&gt;environment variable, which we configured in the Val Town settings */&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;anthropic&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-smi"&gt;Anthropic&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We're using the very latest release of the &lt;a href="https://github.com/anthropics/anthropic-sdk-typescript"&gt;Anthropic TypeScript SDK&lt;/a&gt;, which came out just &lt;a href="https://github.com/anthropics/anthropic-sdk-typescript/commits/sdk-v0.24.0/"&gt;a few hours prior&lt;/a&gt; to recording the livestream.&lt;/p&gt;
&lt;p&gt;I set the &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; environment variable to my Claude 3 API key in the Val Town settings, making it available to all of my Vals. The &lt;code&gt;Anthropic()&lt;/code&gt; constructor picks that up automatically.&lt;/p&gt;
&lt;p&gt;Next, the function to suggest keywords for a user's question:&lt;/p&gt;
&lt;div class="highlight highlight-source-ts"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;suggestKeywords&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;question&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Takes a question like "What is shot-scraper?" and asks 3.5 Sonnet&lt;/span&gt;
  &lt;span class="pl-c"&gt;// to suggest individual search keywords to help answer the question.&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;message&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;anthropic&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;messages&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;create&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;max_tokens&lt;/span&gt;: &lt;span class="pl-c1"&gt;128&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;model&lt;/span&gt;: &lt;span class="pl-s"&gt;"claude-3-5-sonnet-20240620"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c"&gt;// The tools option enforces a JSON schema array of strings&lt;/span&gt;
    &lt;span class="pl-c1"&gt;tools&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;"suggested_search_keywords"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;description&lt;/span&gt;: &lt;span class="pl-s"&gt;"Suggest individual search keywords to help answer the question."&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;input_schema&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"object"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
        &lt;span class="pl-c1"&gt;properties&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
          &lt;span class="pl-c1"&gt;keywords&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
            &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"array"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;items&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
              &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"string"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;description&lt;/span&gt;: &lt;span class="pl-s"&gt;"List of suggested single word search keywords"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
        &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
        &lt;span class="pl-c1"&gt;required&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"keywords"&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c"&gt;// This forces it to always run the suggested_search_keywords tool&lt;/span&gt;
    &lt;span class="pl-c1"&gt;tool_choice&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"tool"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;"suggested_search_keywords"&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;messages&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
      &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;role&lt;/span&gt;: &lt;span class="pl-s"&gt;"user"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-s1"&gt;question&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// This helped TypeScript complain less about accessing .input.keywords&lt;/span&gt;
  &lt;span class="pl-c"&gt;// since it knows this object can be one of two different types&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;type&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s"&gt;"text"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-k"&gt;throw&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-smi"&gt;Error&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;input&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;keywords&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We're asking Claude 3.5 Sonnet here to suggest individual search keywords to help answer that question. I tried Claude 3 Haiku first but it didn't reliably return single word keywords - Sonnet 3.5 followed the "single word search keywords" instruction better.&lt;/p&gt;
&lt;p&gt;This function also uses Claude tools to enforce a response in a JSON schema that specifies an array of strings. More on how I wrote that code (with Claude's assistance) later on.&lt;/p&gt;
&lt;p&gt;Next, the code to run the search itself against Datasette:&lt;/p&gt;
&lt;div class="highlight highlight-source-ts"&gt;&lt;pre&gt;&lt;span class="pl-c"&gt;// The SQL query from earlier&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;sql&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;`select&lt;/span&gt;
&lt;span class="pl-s"&gt;  blog_entry.id,&lt;/span&gt;
&lt;span class="pl-s"&gt;  blog_entry.title,&lt;/span&gt;
&lt;span class="pl-s"&gt;  blog_entry.body,&lt;/span&gt;
&lt;span class="pl-s"&gt;  blog_entry.created&lt;/span&gt;
&lt;span class="pl-s"&gt;from&lt;/span&gt;
&lt;span class="pl-s"&gt;  blog_entry&lt;/span&gt;
&lt;span class="pl-s"&gt;  join blog_entry_fts on blog_entry_fts.rowid = blog_entry.rowid&lt;/span&gt;
&lt;span class="pl-s"&gt;where&lt;/span&gt;
&lt;span class="pl-s"&gt;  blog_entry_fts match :search&lt;/span&gt;
&lt;span class="pl-s"&gt;order by&lt;/span&gt;
&lt;span class="pl-s"&gt;  rank&lt;/span&gt;
&lt;span class="pl-s"&gt;limit&lt;/span&gt;
&lt;span class="pl-s"&gt;  10`&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;runSearch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;keywords&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Turn the keywords into "word1" OR "word2" OR "word3"&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;search&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;keywords&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;s&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s"&gt;`"&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;s&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;"`&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;join&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;" OR "&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Compose the JSON API URL to run the query&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-smi"&gt;URLSearchParams&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
    search&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    sql&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;_shape&lt;/span&gt;: &lt;span class="pl-s"&gt;"array"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;"https://datasette.simonwillison.net/simonwillisonblog.json?"&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Datasette supports read-only SQL queries via its JSON API, which means we can construct the SQL query as a JavaScript string and then encode it as a query string using &lt;code&gt;URLSearchParams()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We also take the list of keywords and turn them into a SQLite FTS search query that looks like &lt;code&gt;"word1" OR "word2" OR "word3"&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;SQLite's built-in relevance calculations work well with this - you can throw in dozens of words separated by &lt;code&gt;OR&lt;/code&gt; and the top ranking results will generally be the ones with the most matches.&lt;/p&gt;
&lt;p&gt;Finally, the code that ties this together - suggests keywords, runs the search and then asks Claude to answer the question. I ended up bundling that together in the HTTP handler for the Val Town script - this is the code that is called for every incoming HTTP request:&lt;/p&gt;
&lt;div class="highlight highlight-source-ts"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;export&lt;/span&gt; &lt;span class="pl-k"&gt;default&lt;/span&gt; &lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;function&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;req&lt;/span&gt;: &lt;span class="pl-smi"&gt;Request&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// This is the Val Town HTTP handler&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-smi"&gt;URL&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;req&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;question&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;url&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;searchParams&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"question"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;slice&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;40&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;!&lt;/span&gt;&lt;span class="pl-s1"&gt;question&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-smi"&gt;Response&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-s"&gt;"error"&lt;/span&gt;: &lt;span class="pl-s"&gt;"No question provided"&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Turn the question into search terms&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;keywords&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;suggestKeywords&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;question&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

  &lt;span class="pl-c"&gt;// Run the actual search&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;runSearch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;keywords&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

  &lt;span class="pl-c"&gt;// Strip HTML tags from each body property, modify in-place:&lt;/span&gt;
  &lt;span class="pl-s1"&gt;result&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;forEach&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;r&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;body&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;replace&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-pds"&gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;&amp;lt;[^&amp;gt;]*&amp;gt;&lt;span class="pl-c1"&gt;/&lt;/span&gt;g&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;""&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

  &lt;span class="pl-c"&gt;// Glue together a string of the title and body properties in one go&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;context&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;r&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;title&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s"&gt;" "&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;join&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"\n\n"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

  &lt;span class="pl-c"&gt;// Ask Claude to answer the question&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;message&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;anthropic&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;messages&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;create&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;max_tokens&lt;/span&gt;: &lt;span class="pl-c1"&gt;1024&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;model&lt;/span&gt;: &lt;span class="pl-s"&gt;"claude-3-haiku-20240307"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;messages&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
      &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;role&lt;/span&gt;: &lt;span class="pl-s"&gt;"user"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-s1"&gt;context&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;role&lt;/span&gt;: &lt;span class="pl-s"&gt;"assistant"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-s"&gt;"Thank you for the context, I am ready to answer your question"&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;role&lt;/span&gt;: &lt;span class="pl-s"&gt;"user"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-s1"&gt;question&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-smi"&gt;Response&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-c1"&gt;answer&lt;/span&gt;: &lt;span class="pl-s1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There are many other ways you could arrange the prompting here. I quite enjoy throwing together a fake conversation like this that feeds in the context and then hints at the agent that it should respond next with its answer, but there are many potential variations on this theme.&lt;/p&gt;
&lt;p&gt;This initial version returned the answer as a JSON object, something like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
    &lt;span class="pl-ent"&gt;"answer"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;shot-scraper is a command-line tool that automates the process of taking screenshots of web pages...&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/claude-rag/frame_010550.jpg" alt="Screenshot of the Val Town interface returning the JSON answer to the question in a preview window" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;We were running out of time, but we wanted to add an HTML interface. Steve suggested getting Claude to write the whole thing! So we tried this:&lt;/p&gt;
&lt;div class="highlight highlight-source-ts"&gt;&lt;pre&gt;  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;message&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;anthropic&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;messages&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;create&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;max_tokens&lt;/span&gt;: &lt;span class="pl-c1"&gt;1024&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;model&lt;/span&gt;: &lt;span class="pl-s"&gt;"claude-3-5-sonnet-20240620"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c"&gt;// "claude-3-haiku-20240307",&lt;/span&gt;
    &lt;span class="pl-c1"&gt;system&lt;/span&gt;: &lt;span class="pl-s"&gt;"Return a full HTML document as your answer, no markdown, make it pretty with exciting relevant CSS"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;messages&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
      &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;role&lt;/span&gt;: &lt;span class="pl-s"&gt;"user"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-s1"&gt;context&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;role&lt;/span&gt;: &lt;span class="pl-s"&gt;"assistant"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-s"&gt;"Thank you for the context, I am ready to answer your question as HTML"&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;role&lt;/span&gt;: &lt;span class="pl-s"&gt;"user"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-s1"&gt;question&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Return back whatever HTML Claude gave us&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-smi"&gt;Response&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;status&lt;/span&gt;: &lt;span class="pl-c1"&gt;200&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;headers&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-s"&gt;"Content-Type"&lt;/span&gt;: &lt;span class="pl-s"&gt;"text/html"&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We upgraded to 3.5 Sonnet to see if it had better "taste" than Haiku, and the results were really impressive. Here's what it gave us for "What is Datasette?":&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/claude-rag/frame_011319.jpg" alt="Screnshot of a What is Datasette? page created by Claude 3.5 Sonnet - it includes a Key Features section with four different cards arranged in a grid, for Explore Data, Publish Data, API Access and Extensible." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;It even styled the page with flexbox to arrange the key features of Datasette in a 2x2 grid! You can see that in the video at &lt;a href="https://www.youtube.com/watch?v=9pmC3P1fUFo&amp;amp;t=1h13m17s"&gt;1h13m17s&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There's a &lt;a href="https://gist.github.com/simonw/7f8db0c452378eb4fa4747196b8194dc"&gt;full copy of the final TypeScript code&lt;/a&gt; available in a Gist.&lt;/p&gt;
&lt;h4 id="tricks-along-the-way"&gt;Some tricks we used along the way&lt;/h4&gt;
&lt;p&gt;I didn't write all of the above code. Some bits of it were written by pasting things into Claude 3.5 Sonnet, and others used the &lt;a href="https://codeium.com/"&gt;Codeium&lt;/a&gt; integration in the Val Town editor (&lt;a href="https://blog.val.town/blog/val-town-newsletter-16/#-codeium-completions"&gt;described here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;One pattern that worked particularly well was getting Sonnet to write the tool-using TypeScript code for us.&lt;/p&gt;
&lt;p&gt;The Claude 3 documentation showed &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/tool-use"&gt;how to do that using curl&lt;/a&gt;. I pasted that &lt;code&gt;curl&lt;/code&gt; example in, added some example TypeScript and then prompted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Guess the JavaScript for setting up a tool which just returns a list of strings, called suggested_search_keywords&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's my full prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
IMAGE_URL="https://upload.wikimedia.org/wikipedia/commons/a/a7/Camponotus_flavomarginatus_ant.jpg"
IMAGE_MEDIA_TYPE="image/jpeg"
IMAGE_BASE64=$(curl "$IMAGE_URL" | base64)
curl https://api.anthropic.com/v1/messages \
     --header "content-type: application/json" \
     --header "x-api-key: $ANTHROPIC_API_KEY" \
     --header "anthropic-version: 2023-06-01" \
     --data \
'{
    "model": "claude-3-sonnet-20240229",
    "max_tokens": 1024,
    "tools": [{
        "name": "record_summary",
        "description": "Record summary of an image using well-structured JSON.",
        "input_schema": {
            "type": "object",
            "properties": {
                "key_colors": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "r": { "type": "number", "description": "red value [0.0, 1.0]" },
                            "g": { "type": "number", "description": "green value [0.0, 1.0]" },
                            "b": { "type": "number", "description": "blue value [0.0, 1.0]" },
                            "name": { "type": "string", "description": "Human-readable color name in snake_case, e.g. \"olive_green\" or \"turquoise\"" }
                        },
                        "required": [ "r", "g", "b", "name" ]
                    },
                    "description": "Key colors in the image. Limit to less then four."
                },
                "description": {
                    "type": "string",
                    "description": "Image description. One to two sentences max."
                },
                "estimated_year": {
                    "type": "integer",
                    "description": "Estimated year that the images was taken, if is it a photo. Only set this if the image appears to be non-fictional. Rough estimates are okay!"
                }
            },
            "required": [ "key_colors", "description" ]
        }
    }],
    "tool_choice": {"type": "tool", "name": "record_summary"},
    "messages": [
        {"role": "user", "content": [
            {"type": "image", "source": {
                "type": "base64",
                "media_type": "'$IMAGE_MEDIA_TYPE'",
                "data": "'$IMAGE_BASE64'"
            }},
            {"type": "text", "text": "Describe this image."}
        ]}
    ]
}'

Based on that example and this JavaScript code:

const anthropic = new Anthropic();
const message = await anthropic.messages.create({
  max_tokens: 1024,
  system: "Suggest individual search keywords to help answer this question. No yapping.",
  messages: [
    { role: "user", content: question },
  ],
  model: "claude-3-haiku-20240307",
});
console.log(message.content[0].text);

Guess the JavaScript for setting up a tool which just returns a list of strings, called suggested_search_keywords
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It gave me back this, which was the &lt;em&gt;exact&lt;/em&gt; code I needed to make my tool-enabled API call from Val Town:&lt;/p&gt;
&lt;div class="highlight highlight-source-ts"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;anthropic&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-smi"&gt;Anthropic&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;message&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;anthropic&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;messages&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;create&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c1"&gt;max_tokens&lt;/span&gt;: &lt;span class="pl-c1"&gt;1024&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;model&lt;/span&gt;: &lt;span class="pl-s"&gt;"claude-3-haiku-20240307"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;tools&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;"suggested_search_keywords"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;description&lt;/span&gt;: &lt;span class="pl-s"&gt;"Suggest individual search keywords to help answer the question."&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;input_schema&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"object"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;properties&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-c1"&gt;keywords&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
          &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"array"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-c1"&gt;items&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
            &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"string"&lt;/span&gt;
          &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-c1"&gt;description&lt;/span&gt;: &lt;span class="pl-s"&gt;"List of suggested search keywords"&lt;/span&gt;
        &lt;span class="pl-kos"&gt;}&lt;/span&gt;
      &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;required&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"keywords"&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;tool_choice&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"tool"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;"suggested_search_keywords"&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;messages&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
    &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;role&lt;/span&gt;: &lt;span class="pl-s"&gt;"user"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;content&lt;/span&gt;: &lt;span class="pl-s1"&gt;question&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-kos"&gt;]&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

&lt;span class="pl-smi"&gt;console&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;log&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;message&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;content&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;text&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I really like this trick, and I use it often in my own work. Find some example code that illustrates the pattern you need, paste in some additional context and get the LLM to figure the rest out for you.&lt;/p&gt;
&lt;h4 id="just-a-prototype"&gt;This is just a prototype&lt;/h4&gt;
&lt;p&gt;It's important to reiterate that this is just a prototype - it's the version of search-backed RAG I could get working in an hour.&lt;/p&gt;
&lt;p&gt;Putting something like this into production requires a whole lot more work. Most importantly, good RAG systems are backed by evals - it's extremely hard to iterate on and improve a system like this if you don't have a good mechanism in place to evaluate if your changes are making things better or not. &lt;a href="https://hamel.dev/blog/posts/evals/"&gt;Your AI Product Needs Evals&lt;/a&gt; by Hamel Husain remains my favourite piece of writing on how to go about putting these together.&lt;/p&gt;
&lt;h4 id="additional-links-from-livestream"&gt;Additional links from the livestream&lt;/h4&gt;
&lt;p&gt;Here are some of the other projects and links mentioned during our conversation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; and its &lt;a href="https://datasette.io/plugins"&gt;150+ plugins&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;My original idea for a project was to use the &lt;a href="https://docs.datasette.io/en/latest/json_api.html#the-json-write-api"&gt;Datasette Write API&lt;/a&gt; and run scheduled Vals to import data from various sources (my toots, tweets, posts etc) into a single searchable table.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; - my command-line utility for working with different language models.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://shot-scraper.datasette.io/"&gt;shot-scraper&lt;/a&gt; for automating screenshots and scraping websites with JavaScript from the command-line - here's &lt;a href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003715.jpg"&gt;a recent demo&lt;/a&gt; where I scraped Google using shot-scraper and fed the results into LLM as a basic form of RAG.&lt;/li&gt;
&lt;li&gt;My current list of &lt;a href="https://github.com/simonw/simonw/blob/main/releases.md"&gt;277 projects with at least one release&lt;/a&gt; on GitHub.&lt;/li&gt;
&lt;li&gt;My &lt;a href="https://til.simonwillison.net/"&gt;TIL blog&lt;/a&gt;, which runs on a templated version of Datasette - &lt;a href="https://til.simonwillison.net/tils"&gt;here's the database&lt;/a&gt; and &lt;a href=""&gt;here's the GitHub Actions workflow that builds it&lt;/a&gt; using the &lt;a href="https://simonwillison.net/2021/Jul/28/baked-data/"&gt;Baked Data pattern&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;I have some previous experiments using embeddings with Datasette, including a &lt;a href="https://til.simonwillison.net/tils/embeddings"&gt;table of embeddings&lt;/a&gt; (encoded &lt;a href="https://llm.datasette.io/en/stable/embeddings/storage.html"&gt;like this&lt;/a&gt;) for my TIL blog which I use to power related items. That's described in this TIL: &lt;a href="https://til.simonwillison.net/llms/openai-embeddings-related-content"&gt;Storing and serving related documents with openai-to-sqlite and embeddings&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/claude-3-5-sonnet"&gt;claude-3-5-sonnet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/my-talks"&gt;my-talks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rag"&gt;rag&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/val-town"&gt;val-town&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/steve-krouse"&gt;steve-krouse&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-to-app"&gt;prompt-to-app&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-artifacts"&gt;claude-artifacts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-talks"&gt;annotated-talks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-engineering"&gt;prompt-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-search"&gt;ai-assisted-search&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="claude-3-5-sonnet"/><category term="my-talks"/><category term="rag"/><category term="projects"/><category term="datasette"/><category term="claude"/><category term="anthropic"/><category term="llms"/><category term="val-town"/><category term="steve-krouse"/><category term="prompt-to-app"/><category term="claude-artifacts"/><category term="annotated-talks"/><category term="prompt-engineering"/><category term="ai"/><category term="generative-ai"/><category term="ai-assisted-search"/><category term="ai-assisted-programming"/></entry><entry><title>Val Vibes: Semantic search in Val Town</title><link href="https://simonwillison.net/2024/Jun/21/semantic-search-in-val-town/#atom-tag" rel="alternate"/><published>2024-06-21T02:16:10+00:00</published><updated>2024-06-21T02:16:10+00:00</updated><id>https://simonwillison.net/2024/Jun/21/semantic-search-in-val-town/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.val.town/blog/val-vibes/"&gt;Val Vibes: Semantic search in Val Town&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A neat case-study by JP Posma on how Val Town's developers can use Val Town Vals to build prototypes of new features that later make it into Val Town core.&lt;/p&gt;
&lt;p&gt;This one explores building out &lt;a href="https://www.val.town/search?searchType=semantic"&gt;semantic search&lt;/a&gt; against Vals using OpenAI embeddings and the PostgreSQL pgvector extension.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/embeddings"&gt;embeddings&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/val-town"&gt;val-town&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-search"&gt;ai-assisted-search&lt;/a&gt;&lt;/p&gt;



</summary><category term="postgresql"/><category term="ai"/><category term="openai"/><category term="embeddings"/><category term="val-town"/><category term="ai-assisted-search"/></entry><entry><title>Val Town Newsletter 15</title><link href="https://simonwillison.net/2024/Feb/15/val-town-newsletter-15/#atom-tag" rel="alternate"/><published>2024-02-15T16:26:09+00:00</published><updated>2024-02-15T16:26:09+00:00</updated><id>https://simonwillison.net/2024/Feb/15/val-town-newsletter-15/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.val.town/blog/val-town-newsletter-15/"&gt;Val Town Newsletter 15&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I really like how Val Town founder Steve Krouse now accompanies their “what’s new” newsletter with a video tour of the new features. I’m seriously considering imitating this for my own projects.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/video"&gt;video&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/val-town"&gt;val-town&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/steve-krouse"&gt;steve-krouse&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="video"/><category term="val-town"/><category term="steve-krouse"/></entry><entry><title>The first four Val Town runtimes</title><link href="https://simonwillison.net/2024/Feb/8/the-first-four-val-town-runtimes/#atom-tag" rel="alternate"/><published>2024-02-08T18:38:39+00:00</published><updated>2024-02-08T18:38:39+00:00</updated><id>https://simonwillison.net/2024/Feb/8/the-first-four-val-town-runtimes/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.val.town/blog/first-four-val-town-runtimes/"&gt;The first four Val Town runtimes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Val Town solves one of my favourite technical problems: how to run untrusted code in a safe sandbox. They're on their fourth iteration of this now, currently using a Node.js application that launches Deno sub-processes using the &lt;a href="https://github.com/casual-simulation/node-deno-vm"&gt;node-deno-vm&lt;/a&gt; npm package and runs code in those, taking advantage of the Deno sandboxing mechanism and terminating processes that take too long in order to protect against &lt;code&gt;while(true)&lt;/code&gt; style attacks.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/tmcw/status/1755616125474504960"&gt;@tmcw&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nodejs"&gt;nodejs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sandboxing"&gt;sandboxing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/deno"&gt;deno&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tom-macwright"&gt;tom-macwright&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/val-town"&gt;val-town&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="nodejs"/><category term="sandboxing"/><category term="deno"/><category term="tom-macwright"/><category term="val-town"/></entry></feed>