<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: steve-krouse</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/steve-krouse.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2025-11-12T17:21:19+00:00</updated><author><name>Simon Willison</name></author><entry><title>Quoting Steve Krouse</title><link href="https://simonwillison.net/2025/Nov/12/steve-krouse/#atom-tag" rel="alternate"/><published>2025-11-12T17:21:19+00:00</published><updated>2025-11-12T17:21:19+00:00</updated><id>https://simonwillison.net/2025/Nov/12/steve-krouse/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://x.com/stevekrouse/status/1988641250329989533"&gt;&lt;p&gt;The fact that MCP is a difference surface from your normal API allows you to ship MUCH faster to MCP. This has been unlocked by inference at runtime&lt;/p&gt;
&lt;p&gt;Normal APIs are promises to developers, because developer commit code that relies on those APIs, and then walk away. If you break the API, you break the promise, and you break that code. This means a developer gets woken up at 2am to fix the code&lt;/p&gt;
&lt;p&gt;But MCP servers are called by LLMs which dynamically read the spec every time, which allow us to constantly change the MCP server. It doesn't matter! We haven't made any promises. The LLM can figure it out afresh every time&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://x.com/stevekrouse/status/1988641250329989533"&gt;Steve Krouse&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/apis"&gt;apis&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/steve-krouse"&gt;steve-krouse&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/model-context-protocol"&gt;model-context-protocol&lt;/a&gt;&lt;/p&gt;



</summary><category term="apis"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="steve-krouse"/><category term="model-context-protocol"/></entry><entry><title>Quoting Steve Krouse</title><link href="https://simonwillison.net/2025/Jul/30/steve-krouse/#atom-tag" rel="alternate"/><published>2025-07-30T21:21:16+00:00</published><updated>2025-07-30T21:21:16+00:00</updated><id>https://simonwillison.net/2025/Jul/30/steve-krouse/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://blog.val.town/vibe-code"&gt;&lt;p&gt;When you vibe code, you are incurring tech debt as fast as the LLM can spit it out. Which is why vibe coding is &lt;em&gt;perfect&lt;/em&gt; for prototypes and throwaway projects: It's only legacy code if you have to maintain it!&amp;nbsp;[...]&lt;/p&gt;
&lt;p&gt;The worst possible situation is to have a non-programmer vibe code a large project that they intend to maintain. This would be the equivalent of giving a credit card to a child without first explaining the concept of debt. [...]&lt;/p&gt;
&lt;p&gt;If you don't understand the code, your only recourse is to ask AI to fix it for you, which is like paying off credit card debt with another credit card.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://blog.val.town/vibe-code"&gt;Steve Krouse&lt;/a&gt;, Vibe code is legacy code&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/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/technical-debt"&gt;technical-debt&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/vibe-coding"&gt;vibe-coding&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="technical-debt"/><category term="steve-krouse"/><category term="vibe-coding"/></entry><entry><title>Quoting Steve Krouse</title><link href="https://simonwillison.net/2025/May/31/steve-krouse/#atom-tag" rel="alternate"/><published>2025-05-31T14:26:08+00:00</published><updated>2025-05-31T14:26:08+00:00</updated><id>https://simonwillison.net/2025/May/31/steve-krouse/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://twitter.com/stevekrouse/status/1928818847764582698"&gt;&lt;p&gt;There's a new kind of coding I call "hype coding" where you fully give into the hype, and what's coming right around the corner, that you lose sight of whats' possible today. Everything is changing so fast that nobody has time to learn any tool, but we should aim to use as many as possible. Any limitation in the technology can be chalked up to a 'skill issue' or that it'll be solved in the next AI release next week. Thinking is dead. Turn off your brain and let the computer think for you. Scroll on tiktok while the armies of agents code for you. If it isn't right, tell it to try again. Don't read. Feed outputs back in until it works. If you can't get it to work, wait for the next model or tool release. Maybe you didn't use enough MCP servers? Don't forget to add to the hype cycle by aggrandizing all your successes. Don't read this whole tweet, because it's too long. Get an AI to summarize it for you. Then call it "cope". Most importantly, immediately mischaracterize "hype coding" to mean something different than this definition. Oh the irony! The people who don't care about details don't read the details about not reading the details&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://twitter.com/stevekrouse/status/1928818847764582698"&gt;Steve Krouse&lt;/a&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/steve-krouse"&gt;steve-krouse&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/model-context-protocol"&gt;model-context-protocol&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/semantic-diffusion"&gt;semantic-diffusion&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai"/><category term="steve-krouse"/><category term="vibe-coding"/><category term="model-context-protocol"/><category term="semantic-diffusion"/></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>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 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></feed>