<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: files-to-prompt</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/files-to-prompt.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2025-04-07T17:45:11+00:00</updated><author><name>Simon Willison</name></author><entry><title>Long context support in LLM 0.24 using fragments and template plugins</title><link href="https://simonwillison.net/2025/Apr/7/long-context-llm/#atom-tag" rel="alternate"/><published>2025-04-07T17:45:11+00:00</published><updated>2025-04-07T17:45:11+00:00</updated><id>https://simonwillison.net/2025/Apr/7/long-context-llm/#atom-tag</id><summary type="html">
    &lt;p&gt;LLM 0.24 is &lt;a href="https://llm.datasette.io/en/stable/changelog.html#v0-24"&gt;now available&lt;/a&gt; with new features to help take advantage of the increasingly long input context supported by modern LLMs.&lt;/p&gt;
&lt;p&gt;(&lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; is my command-line tool and &lt;a href="https://llm.datasette.io/en/stable/python-api.html"&gt;Python library&lt;/a&gt; for interacting with LLMs, supported by &lt;a href="https://llm.datasette.io/en/stable/plugins/directory.html"&gt;20+ plugins&lt;/a&gt; adding support for both local and remote models from a bunch of different providers.)&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Apr/7/long-context-llm/#trying-it-out"&gt;Trying it out&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Apr/7/long-context-llm/#improving-llm-s-support-for-long-context-models"&gt;Improving LLM's support for long context models&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Apr/7/long-context-llm/#asking-questions-of-llm-s-documentation"&gt;Asking questions of LLM's documentation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Apr/7/long-context-llm/#publishing-sharing-and-reusing-templates"&gt;Publishing, sharing and reusing templates&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Apr/7/long-context-llm/#template-and-fragment-loader-plugins"&gt;Template and fragment loader plugins so far&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Apr/7/long-context-llm/#everything-else-in-llm-0-24"&gt;Everything else in LLM 0.24&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h4 id="trying-it-out"&gt;Trying it out&lt;/h4&gt;

&lt;p&gt;To install LLM with &lt;a href="https://github.com/astral-sh/uv"&gt;uv&lt;/a&gt; (there are &lt;a href="https://llm.datasette.io/en/stable/setup.html#installation"&gt;several other options&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uv tool install llm&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You'll need to either provide &lt;a href="https://platform.openai.com/api-keys"&gt;an OpenAI API key&lt;/a&gt; or install &lt;a href="https://llm.datasette.io/en/stable/plugins/directory.html"&gt;a plugin&lt;/a&gt; to use local models or  models from other providers:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm keys &lt;span class="pl-c1"&gt;set&lt;/span&gt; openai
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Paste OpenAI API key here&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To &lt;a href="https://llm.datasette.io/en/stable/setup.html#upgrading-to-the-latest-version"&gt;upgrade LLM&lt;/a&gt; from a previous version:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm install -U llm&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The biggest new feature is &lt;strong&gt;&lt;a href="https://llm.datasette.io/en/stable/fragments.html"&gt;fragments&lt;/a&gt;&lt;/strong&gt;. You can now use &lt;code&gt;-f filename&lt;/code&gt; or &lt;code&gt;-f url&lt;/code&gt; to add one or more fragments to your prompt, which means you can do things like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm -f https://simonwillison.net/2025/Apr/5/llama-4-notes/ &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;bullet point summary&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/7ffb19f647603558c575359e678185c0"&gt;the output&lt;/a&gt; from that prompt, exported using &lt;code&gt;llm logs -c --expand --usage&lt;/code&gt;. Token cost was 5,372 input, 374 output which works out as 0.103 cents (around 1/10th of a cent) using the default GPT-4o mini model.&lt;/p&gt;
&lt;p&gt;Plugins can implement custom fragment loaders with a prefix. The &lt;a href="https://github.com/simonw/llm-fragments-github"&gt;llm-fragments-github&lt;/a&gt; plugin adds a &lt;code&gt;github:&lt;/code&gt; prefix that can be used to load every text file in a GitHub repository as a list of fragments:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm install llm-fragments-github
llm -f github:simonw/s3-credentials &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Suggest new features for this tool&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/c9bbbc5f6560b01f4b7882ac0194fb25"&gt;the output&lt;/a&gt;. That took 49,856 input tokens for a total cost of 0.7843 cents - nearly a whole cent!&lt;/p&gt;
&lt;h4 id="improving-llm-s-support-for-long-context-models"&gt;Improving LLM's support for long context models&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://simonwillison.net/tags/long-context/"&gt;Long context&lt;/a&gt;&lt;/strong&gt; is one of the most exciting trends in LLMs over the past eighteen months. Saturday's &lt;a href="https://simonwillison.net/2025/Apr/5/llama-4-notes/"&gt;Llama 4 Scout release&lt;/a&gt; gave us the first model with a full 10 million token context. Google's &lt;a href="https://simonwillison.net/tags/gemini/"&gt;Gemini&lt;/a&gt; family has several 1-2 million token models, and the baseline for recent models from both OpenAI and Anthropic is 100 or 200 thousand.&lt;/p&gt;
&lt;p&gt;Two years ago most models capped out at 8,000 tokens of input. Long context opens up many new interesting ways to apply this class of technology.&lt;/p&gt;
&lt;p&gt;I've been using long context models via &lt;a href="https://simonwillison.net/tags/files-to-prompt/"&gt;my files-to-prompt tool&lt;/a&gt; to summarize large codebases, explain how they work and even to &lt;a href="https://simonwillison.net/2024/Sep/25/o1-preview-llm/"&gt;debug gnarly bugs&lt;/a&gt;. As demonstrated above, it's surprisingly inexpensive to drop tens of thousands of tokens into  models like GPT-4o mini or most of the Google Gemini series, and the results are often very impressive.&lt;/p&gt;
&lt;p&gt;One of LLM's most useful features is that it &lt;a href="https://llm.datasette.io/en/stable/logging.html"&gt;logs every prompt and response&lt;/a&gt; to a SQLite database. This is great for comparing the same prompt against different models and tracking experiments over time - my own database contained thousands of responses from hundreds of different models accumulated over the past couple of years.&lt;/p&gt;
&lt;p&gt;This is where long context prompts were starting to be a problem. Since LLM stores the full prompt and response in the database, asking five questions of the same source code could result in five duplicate copies of that text in the database!&lt;/p&gt;
&lt;p&gt;The new &lt;strong&gt;fragments&lt;/strong&gt; feature targets this problem head on. Each fragment is stored once in a &lt;a href="https://llm.datasette.io/en/stable/logging.html#sql-schema"&gt;fragments&lt;/a&gt; table, then de-duplicated in the future using a SHA256 hash of its content.&lt;/p&gt;
&lt;p&gt;This saves on storage, and also enables features like &lt;code&gt;llm logs -f X&lt;/code&gt; for seeing all logged responses &lt;a href="https://llm.datasette.io/en/stable/logging.html#filtering-by-prompts-that-used-a-specific-fragment"&gt;that use a particular fragment&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Fragments can be specified in several different ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a path to a file&lt;/li&gt;
&lt;li&gt;a URL to data online&lt;/li&gt;
&lt;li&gt;an alias that's been set against a previous fragment (see &lt;a href="https://llm.datasette.io/en/latest/help.html#llm-fragments-set-help"&gt;llm fragments set&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;a hash ID of the content of a fragment&lt;/li&gt;
&lt;li&gt;using &lt;code&gt;prefix:argument&lt;/code&gt; to specify fragments from a plugin&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="asking-questions-of-llm-s-documentation"&gt;Asking questions of LLM's documentation&lt;/h4&gt;
&lt;p&gt;Wouldn't it be neat if LLM could answer questions about its own documentation?&lt;/p&gt;
&lt;p&gt;The new &lt;a href="https://github.com/simonw/llm-docs"&gt;llm-docs&lt;/a&gt; plugin (built with the new &lt;a href="https://llm.datasette.io/en/stable/plugins/plugin-hooks.html#register-fragment-loaders-register"&gt;register_fragment_loaders() plugin hook&lt;/a&gt;) enables exactly that:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm install llm-docs
llm -f docs: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;How do I embed a binary file?&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;a href="https://gist.github.com/simonw/df115ffa63e101833a100f71555bf53d"&gt;output&lt;/a&gt; starts like this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To embed a binary file using the LLM command-line interface, you can use the &lt;code&gt;llm embed&lt;/code&gt; command with the &lt;code&gt;--binary&lt;/code&gt; option. Here’s how you can do it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Make sure you have the appropriate embedding model installed that supports binary input.&lt;/li&gt;
&lt;li&gt;Use the following command syntax:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm embed -m &lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;model_id&lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt; --binary -i &lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;path_to_your_binary_file&lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Replace &lt;code&gt;&amp;lt;model_id&amp;gt;&lt;/code&gt; with the identifier for the embedding model you want to use (e.g., &lt;code&gt;clip&lt;/code&gt; for the CLIP model) and &lt;code&gt;&amp;lt;path_to_your_binary_file&amp;gt;&lt;/code&gt; with the path to your actual binary file.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(74,570 input, 240 output = 1.1329 cents with GPT-4o mini)&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;-f docs:&lt;/code&gt; with just the prefix is the same as using &lt;code&gt;-f docs:llm&lt;/code&gt;. The plugin fetches the documentation for your current version of LLM from my new &lt;a href="https://github.com/simonw/docs-for-llms"&gt;simonw/docs-for-llms&lt;/a&gt; repo, which also provides packaged documentation files for my &lt;code&gt;datasette&lt;/code&gt;, &lt;code&gt;s3-credentials&lt;/code&gt;, &lt;code&gt;shot-scraper&lt;/code&gt; and &lt;code&gt;sqlite-utils&lt;/code&gt; projects.&lt;/p&gt;
&lt;p&gt;Datasette's documentation has got pretty long, so you might need to run that through a Gemini model instead (using the &lt;a href="https://github.com/simonw/llm-gemini"&gt;llm-gemini plugin&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm -f docs:datasette -m gemini-2.0-flash \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Build a render_cell plugin that detects and renders markdown&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/simonw/f08b3832c379ca89d8cff3c61ec5662e"&gt;Here's the output&lt;/a&gt;. 132,042 input, 1,129 output with Gemini 2.0 Flash = 1.3656 cents.&lt;/p&gt;
&lt;p&gt;You can browse the combined documentation files this uses in &lt;a href="https://github.com/simonw/docs-for-llms"&gt;docs-for-llm&lt;/a&gt;. They're built using GitHub Actions.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://llmstxt.org/"&gt;llms-txt&lt;/a&gt; is a project lead by Jeremy Howard that encourages projects to publish similar files to help LLMs ingest a succinct copy of their documentation.&lt;/p&gt;
&lt;h4 id="publishing-sharing-and-reusing-templates"&gt;Publishing, sharing and reusing templates&lt;/h4&gt;
&lt;p&gt;The new &lt;a href="https://llm.datasette.io/en/stable/plugins/plugin-hooks.html#register-template-loaders-register"&gt;register_template_loaders() plugin hook&lt;/a&gt; allows plugins to register &lt;code&gt;prefix:value&lt;/code&gt; custom template loaders, for use with the &lt;code&gt;llm -t&lt;/code&gt; option.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/llm-templates-github"&gt;llm-templates-github&lt;/a&gt; and &lt;a href="https://github.com/simonw/llm-templates-fabric"&gt;llm-templates-fabric&lt;/a&gt; are two new plugins that make use of that hook.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;llm-templates-github&lt;/code&gt; lets you share and use templates via a public GitHub repository. Here's how to run my &lt;a href="https://simonwillison.net/tags/pelican-riding-a-bicycle/"&gt;Pelican riding a bicycle&lt;/a&gt; benchmark against a specific model:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm install llm-templates-github
llm -t gh:simonw/pelican-svg -m o3-mini&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This executes &lt;a href="https://github.com/simonw/llm-templates/blob/main/pelican-svg.yaml"&gt;this pelican-svg.yaml&lt;/a&gt; template stored in my &lt;a href="https://github.com/simonw/llm-templates"&gt;simonw/llm-templates&lt;/a&gt; repository, using a new repository naming convention.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;llm -t gh:simonw/pelican-svg&lt;/code&gt; will load that &lt;code&gt;pelican-svg.yaml&lt;/code&gt; file from the &lt;code&gt;simonw/llm-templates&lt;/code&gt; repo. You can also use &lt;code&gt;llm -t gh:simonw/name-of-repo/name-of-template&lt;/code&gt; to load a template from a repository that doesn't follow that convention.&lt;/p&gt;
&lt;p&gt;To share your own templates, create a repository on GitHub under your user account called &lt;code&gt;llm-templates&lt;/code&gt; and start saving &lt;code&gt;.yaml&lt;/code&gt; files to it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/llm-templates-fabric"&gt;llm-templates-fabric&lt;/a&gt; provides a similar mechanism for loading templates from  Daniel Miessler's extensive &lt;a href="https://github.com/danielmiessler/fabric"&gt;fabric collection&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm install llm-templates-fabric
curl https://simonwillison.net/2025/Apr/6/only-miffy/ &lt;span class="pl-k"&gt;|&lt;/span&gt; \
  llm -t f:extract_main_idea&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A conversation with Daniel was the inspiration for this new plugin hook.&lt;/p&gt;

&lt;h4 id="template-and-fragment-loader-plugins"&gt;Template and fragment loader plugins so far&lt;/h4&gt;
&lt;p&gt;Here's the list of fragment and template loader plugins so far from the &lt;a href="https://llm.datasette.io/en/stable/plugins/directory.html#fragments-and-template-loaders"&gt;LLM plugins directory&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-templates-github"&gt;llm-templates-github&lt;/a&gt;&lt;/strong&gt; supports loading templates shared on GitHub, e.g. &lt;code&gt;llm -t gh:simonw/pelican-svg&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-templates-fabric"&gt;llm-templates-fabric&lt;/a&gt;&lt;/strong&gt; provides access to the &lt;a href="https://github.com/danielmiessler/fabric"&gt;Fabric&lt;/a&gt; collection of prompts: &lt;code&gt;cat setup.py | llm -t fabric:explain_code&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-fragments-github"&gt;llm-fragments-github&lt;/a&gt;&lt;/strong&gt; can load entire GitHub repositories in a single operation: &lt;code&gt;llm -f github:simonw/files-to-prompt 'explain this code'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm-hacker-news"&gt;llm-hacker-news&lt;/a&gt;&lt;/strong&gt; imports conversations from Hacker News as fragments: &lt;code&gt;llm -f hn:43615912 'summary with illustrative direct quotes'&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h4 id="everything-else-in-llm-0-24"&gt;Everything else in LLM 0.24&lt;/h4&gt;
&lt;p&gt;LLM 0.24 is a big release, spanning &lt;a href="https://github.com/simonw/llm/compare/0.23...0.24"&gt;51 commits&lt;/a&gt;. The &lt;a href="https://llm.datasette.io/en/stable/changelog.html#v0-24"&gt;release notes&lt;/a&gt; cover everything that's new in full - here are a few of my highlights:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The new &lt;a href="https://github.com/simonw/llm-openai-plugin"&gt;llm-openai plugin&lt;/a&gt; provides support for &lt;strong&gt;o1-pro&lt;/strong&gt; (which is not supported by the OpenAI mechanism used by LLM core). Future OpenAI features will migrate to this plugin instead of LLM core itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The problem with OpenAI models being handled by LLM core is that I have to release a whole new version of LLM every time OpenAI releases a new model or feature. Migrating this stuff out to a plugin means I can release new version of that plugin independently of LLM itself - something I frequently do for &lt;a href="https://github.com/simonw/llm-anthropic"&gt;llm-anthropic&lt;/a&gt; and &lt;a href="https://github.com/simonw/llm-gemini"&gt;llm-gemini&lt;/a&gt; and others.&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;llm-openai&lt;/code&gt; plugin uses their Responses API, a new shape of API which &lt;a href="https://simonwillison.net/2025/Mar/11/responses-vs-chat-completions/"&gt;I covered last month&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;llm -t $URL&lt;/code&gt; option can now take a URL to a YAML template. &lt;a href="https://github.com/simonw/llm/issues/856"&gt;#856&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The new custom template loaders are fun, but being able to paste in a URL to a YAML file somewhere provides a simpler way to share templates.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Templates can now store default model options. &lt;a href="https://github.com/simonw/llm/issues/845"&gt;#845&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Attachments can now be stored in templates. &lt;a href="https://github.com/simonw/llm/issues/826"&gt;#826&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The quickest way to create your own template is with the &lt;code&gt;llm prompt ... --save name-of-template&lt;/code&gt; command. This now works with attachments, fragments and default model options, each of which is persisted in &lt;a href="https://llm.datasette.io/en/stable/templates.html#templates-as-yaml-files"&gt;the template YAML file&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;a href="https://llm.datasette.io/en/stable/usage.html#setting-default-options-for-models"&gt;llm models options&lt;/a&gt; family of commands for setting default options for particular models. &lt;a href="https://github.com/simonw/llm/issues/829"&gt;#829&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I built this when I &lt;a href="https://twitter.com/bindureddy/status/1900345517256958140"&gt;learned that&lt;/a&gt; Qwen's QwQ-32b model works best with temperature 0.7 and top p 0.95.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;llm prompt -d path-to-sqlite.db&lt;/code&gt; option can now be used to write logs to a custom SQLite database. &lt;a href="https://github.com/simonw/llm/issues/858"&gt;#858&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This proved extremely useful for testing fragments - it meant I could run a prompt and save the full response to a separate SQLite database which I could then upload to S3 and share &lt;a href="https://github.com/simonw/llm/issues/863#issuecomment-2781723372"&gt;as a link to Datasette Lite&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;llm similar -p/--plain&lt;/code&gt; option providing more human-readable output than the default JSON. &lt;a href="https://github.com/simonw/llm/issues/853"&gt;#853&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I'd like this to be the default output, but I'm holding off on changing that until LLM 1.0 since it's a breaking change for people building automations against the JSON from &lt;code&gt;llm similar&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Set the &lt;code&gt;LLM_RAISE_ERRORS=1&lt;/code&gt; environment variable to raise errors during prompts rather than suppressing them, which means you can run &lt;code&gt;python -i -m llm 'prompt'&lt;/code&gt; and then drop into a debugger on errors with &lt;code&gt;import pdb; pdb.pm()&lt;/code&gt;. &lt;a href="https://github.com/simonw/llm/issues/817"&gt;#817&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Really useful for debugging new model plugins.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;llm prompt -q gpt -q 4o&lt;/code&gt; option - pass &lt;code&gt;-q searchterm&lt;/code&gt; one or more times to execute a prompt against the first model that matches all of those strings - useful for if you can't remember the full model ID. &lt;a href="https://github.com/simonw/llm/issues/841"&gt;#841&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Pretty obscure but I found myself needing this. Vendors love releasing models with names like &lt;code&gt;gemini-2.5-pro-exp-03-25&lt;/code&gt;, now I can run &lt;code&gt;llm -q gem -q 2.5 -q exp 'say hi'&lt;/code&gt; to save me from looking up the model ID.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://llm.datasette.io/en/stable/other-models.html#openai-compatible-models"&gt;OpenAI compatible models&lt;/a&gt; configured using &lt;code&gt;extra-openai-models.yaml&lt;/code&gt; now support &lt;code&gt;supports_schema: true&lt;/code&gt;, &lt;code&gt;vision: true&lt;/code&gt; and &lt;code&gt;audio: true&lt;/code&gt; options. Thanks &lt;a href="https://github.com/adaitche"&gt;@adaitche&lt;/a&gt; and &lt;a href="https://github.com/giuli007"&gt;@giuli007&lt;/a&gt;. &lt;a href="https://github.com/simonw/llm/pull/819"&gt;#819&lt;/a&gt;, &lt;a href="https://github.com/simonw/llm/pull/843"&gt;#843&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I don't use this feature myself but it's clearly popular, this isn't the first time I'e had PRs with improvements from the wider community.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemini"&gt;gemini&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/long-context"&gt;long-context&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="plugins"/><category term="projects"/><category term="ai"/><category term="annotated-release-notes"/><category term="openai"/><category term="generative-ai"/><category term="llms"/><category term="llm"/><category term="gemini"/><category term="long-context"/><category term="files-to-prompt"/></entry><entry><title>Note on 26th March 2025</title><link href="https://simonwillison.net/2025/Mar/26/notes/#atom-tag" rel="alternate"/><published>2025-03-26T06:11:30+00:00</published><updated>2025-03-26T06:11:30+00:00</updated><id>https://simonwillison.net/2025/Mar/26/notes/#atom-tag</id><summary type="html">
    &lt;p&gt;I've added a new content type to my blog: &lt;strong&gt;notes&lt;/strong&gt;. These join my existing types: &lt;a href="https://simonwillison.net/search/?type=entry"&gt;entries&lt;/a&gt;, &lt;a href="https://simonwillison.net/search/?type=blogmark"&gt;bookmarks&lt;/a&gt; and &lt;a href="https://simonwillison.net/search/?type=quotation"&gt;quotations&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;A note is a little bit like a bookmark without a link. They're for short form writing - thoughts or images that don't warrant a full entry with a title. The kind of things I used to post to Twitter, but that don't feel right to cross-post to multiple social networks (Mastodon and Bluesky, for example.)&lt;/p&gt;
&lt;p&gt;I was partly inspired by Molly White's &lt;a href="https://www.mollywhite.net/micro"&gt;short thoughts, notes, links, and musings&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I've been thinking about this for a while, but the amount of work involved in modifying all of the parts of my site that handle the three different content types was daunting. Then this evening I tried running my blog's source code (using &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; and &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt;) through &lt;a href="https://simonwillison.net/2025/Mar/25/gemini/"&gt;the new Gemini 2.5 Pro&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt &lt;span class="pl-c1"&gt;.&lt;/span&gt; -e py -c &lt;span class="pl-k"&gt;|&lt;/span&gt; \
  llm -m gemini-2.5-pro-exp-03-25 -s \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;I want to add a new type of content called a Note,&lt;/span&gt;
&lt;span class="pl-s"&gt;  similar to quotation and bookmark and entry but it&lt;/span&gt;
&lt;span class="pl-s"&gt;  only has a markdown text body. Output all of the&lt;/span&gt;
&lt;span class="pl-s"&gt;  code I need to add for that feature and tell me&lt;/span&gt;
&lt;span class="pl-s"&gt;  which files to add  the code to.&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Gemini gave me &lt;a href="https://gist.github.com/simonw/6d9fb3e33e7105d391a31367d6a235de#response"&gt;a detailed 13 step plan&lt;/a&gt; covering all of the tedious changes I'd been avoiding having to figure out!&lt;/p&gt;
&lt;p&gt;The code &lt;a href="https://github.com/simonw/simonwillisonblog/pull/527"&gt;is in this PR&lt;/a&gt;, which touched 18 different files. The whole project took around 45 minutes start to finish.&lt;/p&gt;
&lt;p&gt;(I used Claude to &lt;a href="https://claude.ai/share/17656d59-6f52-471e-8aeb-6abbe1464471"&gt;brainstorm names&lt;/a&gt; for the feature - I had it come up with possible nouns and then "rank those by least pretentious to most pretentious", and "notes" came out on top.)&lt;/p&gt;
&lt;p&gt;This is now far too long for a note and should really be upgraded to an entry, but I need to post a first note to make sure everything is working as it should.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/molly-white"&gt;molly-white&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/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemini"&gt;gemini&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;



</summary><category term="blogging"/><category term="projects"/><category term="molly-white"/><category term="ai-assisted-programming"/><category term="claude"/><category term="gemini"/><category term="files-to-prompt"/><category term="site-upgrades"/></entry><entry><title>Putting Gemini 2.5 Pro through its paces</title><link href="https://simonwillison.net/2025/Mar/25/gemini/#atom-tag" rel="alternate"/><published>2025-03-25T20:45:58+00:00</published><updated>2025-03-25T20:45:58+00:00</updated><id>https://simonwillison.net/2025/Mar/25/gemini/#atom-tag</id><summary type="html">
    &lt;p&gt;There's a new release from Google Gemini this morning: &lt;a href="https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/#gemini-2-5-thinking"&gt;the first in the Gemini 2.5&lt;/a&gt; series. Google call it "a thinking model, designed to tackle increasingly complex problems". It's already sat at the top of the &lt;a href="https://lmarena.ai/?leaderboard"&gt;LM Arena leaderboard&lt;/a&gt;, and from initial impressions looks like it may deserve that top spot.&lt;/p&gt;
&lt;p&gt;I just released &lt;a href="https://github.com/simonw/llm-gemini/releases/tag/0.16"&gt;llm-gemini 0.16&lt;/a&gt; adding support for the new model to my &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; command-line tool. Let's try it out.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/25/gemini/#the-pelican-riding-a-bicycle"&gt;The pelican riding a bicycle&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/25/gemini/#transcribing-audio"&gt;Transcribing audio&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/25/gemini/#bounding-boxes"&gt;Bounding boxes&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/25/gemini/#characteristics"&gt;More characteristics of the model&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/25/gemini/#gemini-2-5-pro-is-a-very-strong-new-model"&gt;Gemini 2.5 Pro is a very strong new model&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/25/gemini/#update-it-s-very-good-at-code"&gt;Update: it's very good at code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="the-pelican-riding-a-bicycle"&gt;The pelican riding a bicycle&lt;/h4&gt;
&lt;p&gt;First up, my classic &lt;a href="https://simonwillison.net/tags/pelican-riding-a-bicycle/"&gt;generate an SVG of a pelican riding a bicycle&lt;/a&gt; prompt.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Upgrade the plugin&lt;/span&gt;
llm install -U llm-gemini

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Now run the prompt:&lt;/span&gt;
llm -m gemini-2.5-pro-exp-03-25 &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Generate an SVG of a pelican riding a bicycle&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It's pretty solid!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/gemini-2.5-pro-pelican.jpg" alt="The pelican is perched at an angle which suggests an effort at having it make contact with the pedals. It has a good large beak." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/c34f7f0c94afcbeab77e170511f6f51f"&gt;the full transcript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This task is meant to be almost impossible: pelicans are the wrong shape to ride bicycles! Given that, I think this is a good attempt - I like it slightly better than my previous favourite &lt;a href="https://simonwillison.net/2025/Feb/24/claude-37-sonnet-and-claude-code/"&gt;Claude 3.7 Sonnet&lt;/a&gt;, which produced this a month ago:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/pelican-claude-3.7-sonnet.svg" alt="The bicycle has pokes but the pedal is in the wrong place, and the pelican perches uncomfortably on the seat with weird looking wings." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="transcribing-audio"&gt;Transcribing audio&lt;/h4&gt;
&lt;p&gt;I had &lt;a href="https://static.simonwillison.net/static/2025/russian-pelican-in-spanish.mp3"&gt;an MP3&lt;/a&gt; lying around from &lt;a href="https://simonwillison.net/2024/Oct/26/russian-spanish-pelican/"&gt;a previous experiment&lt;/a&gt; which mixes English and Spanish. I tried running it with the prompt &lt;code&gt;transcribe&lt;/code&gt; to see what would happen:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm -m gemini-2.5-pro-exp-03-25 &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;transcribe&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; \
  -a https://static.simonwillison.net/static/2025/russian-pelican-in-spanish.mp3&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I got back this, with timestamps interspersed with the text:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I need you [ 0m0s450ms ] to pretend [ 0m0s880ms ] to be [ 0m0s990ms ] a California [ 0m1s560ms ] brown [ 0m1s850ms ] pelican [ 0m2s320ms ] with [ 0m2s480ms ] a very [ 0m2s990ms ] thick [ 0m3s290ms ] Russian [ 0m3s710ms ] accent, [ 0m4s110ms ] but [ 0m4s540ms ] you [ 0m4s640ms ] talk [ 0m4s830ms ] to me [ 0m4s960ms ] exclusively [ 0m5s660ms ] in Spanish. [ 0m6s200ms ] Oye, [ 0m8s930ms ] camarada, [ 0m9s570ms ] aquí [ 0m10s240ms ] está [ 0m10s590ms ] tu [ 0m10s740ms ] pelícano [ 0m11s370ms ] californiano [ 0m12s320ms ] con [ 0m12s520ms ] acento [ 0m13s100ms ] ruso. [ 0m13s540ms ] Qué [ 0m14s230ms ] tal, [ 0m14s570ms ] tovarisch? [ 0m15s210ms ] Listo [ 0m15s960ms ] para [ 0m16s190ms ] charlar [ 0m16s640ms ] en [ 0m16s750ms ] español? [ 0m17s250ms ] How's [ 0m19s834ms ] your [ 0m19s944ms ] day [ 0m20s134ms ] today? [ 0m20s414ms ] Mi [ 0m22s654ms ] día [ 0m22s934ms ] ha [ 0m23s4ms ] sido [ 0m23s464ms ] volando [ 0m24s204ms ] sobre [ 0m24s594ms ] las [ 0m24s844ms ] olas, [ 0m25s334ms ] buscando [ 0m26s264ms ] peces [ 0m26s954ms ] y [ 0m27s84ms ] disfrutando [ 0m28s14ms ] del [ 0m28s244ms ] sol [ 0m28s664ms ] californiano. [ 0m29s444ms ] Y [ 0m30s314ms ] tú, [ 0m30s614ms ] amigo, ¿ [ 0m31s354ms ] cómo [ 0m31s634ms ] ha [ 0m31s664ms ] estado [ 0m31s984ms ] tu [ 0m32s134ms ] día? [ 0m32s424ms ]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This inspired me to try again, this time including &lt;a href="https://simonwillison.net/2025/Feb/28/llm-schemas/"&gt;a JSON schema&lt;/a&gt; (using LLM's custom schema DSL):&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm -m gemini-2.5-pro-exp-03-25 &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;transcribe&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; \
  -a https://static.simonwillison.net/static/2025/russian-pelican-in-spanish.mp3 \
  --schema-multi &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;timestamp str: mm:ss,text, language: two letter code&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I got an &lt;em&gt;excellent&lt;/em&gt; response from that:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
  &lt;span class="pl-ent"&gt;"items"&lt;/span&gt;: [
    {
      &lt;span class="pl-ent"&gt;"language"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;en&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"text"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;I need you to pretend to be a California brown pelican with a very thick Russian accent, but you talk to me exclusively in Spanish.&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"timestamp"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;00:00&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"language"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;es&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"text"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Oye, camarada. Aquí está tu pelícano californiano con acento ruso.&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"timestamp"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;00:08&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"language"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;es&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"text"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;¿Qué tal, Tovarish? ¿Listo para charlar en español?&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"timestamp"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;00:13&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"language"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;en&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"text"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;How's your day today?&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"timestamp"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;00:19&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"language"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;es&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"text"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Mi día ha sido volando sobre las olas, buscando peces y disfrutando del sol californiano.&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"timestamp"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;00:22&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"language"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;es&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"text"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;¿Y tú, amigo, cómo ha estado tu día?&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"timestamp"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;00:30&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    }
  ]
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I confirmed that the timestamps match the audio. This is fantastic.&lt;/p&gt;
&lt;p&gt;Let's try that against a ten minute snippet of &lt;a href="https://realpython.com/podcasts/rpp/236/"&gt;a podcast episode&lt;/a&gt; I was on:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm -m gemini-2.5-pro-exp-03-25 \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;transcribe, first speaker is Christopher, second is Simon&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; \
  -a ten-minutes-of-podcast.mp3 \
  --schema-multi &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;timestamp str: mm:ss, text, speaker_name&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Useful LLM trick: you can use &lt;code&gt;llm logs -c --data&lt;/code&gt; to get just the JSON data from the most recent prompt response, so I ran this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm logs -c --data &lt;span class="pl-k"&gt;|&lt;/span&gt; jq&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/23d4528e23e4897e0c43458e7c46c360"&gt;the full output JSON&lt;/a&gt;, which starts and ends like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
  &lt;span class="pl-ent"&gt;"items"&lt;/span&gt;: [
    {
      &lt;span class="pl-ent"&gt;"speaker_name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Christopher&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"text"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;on its own and and it has this sort of like a it's like a you know old tree in the forest, you know, kind of thing that you've built, so.&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"timestamp"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;00:00&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"speaker_name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Simon&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"text"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;There's also like I feel like with online writing, never ever like stick something online just expect people to find it. You have to So one of the great things about having a blog is I can be in a conversation about something and somebody ask a question, I can say, oh, I wrote about that two and a half years ago and give people a link.&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"timestamp"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;00:06&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },

    {
      &lt;span class="pl-ent"&gt;"speaker_name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Simon&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"text"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;So on that basis, Chat and I can't remember if the free version of Chat GPT has code interpreter.&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"timestamp"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;09:45&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"speaker_name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Simon&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"text"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;I hope I think it does.&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"timestamp"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;09:50&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    },
    {
      &lt;span class="pl-ent"&gt;"speaker_name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Christopher&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"text"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Okay. So this is like the basic paid one, maybe the $20 month because I know there's like a $200 one that's a little steep for like a basic&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"timestamp"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;09:51&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    }
  ]
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A spot check of the timestamps showed them in the right place. Gemini 2.5 supports long context prompts so it's possible this works well for much longer audio files - it would be interesting to dig deeper and try that out.&lt;/p&gt;
&lt;h4 id="bounding-boxes"&gt;Bounding boxes&lt;/h4&gt;
&lt;p&gt;One of my favourite features of previous Gemini models is their support for bounding boxes: you can prompt them to return boxes around objects in images.&lt;/p&gt;
&lt;p&gt;I built a separate tool for experimenting with this feature in August last year, which I described in &lt;a href="https://simonwillison.net/2024/Aug/26/gemini-bounding-box-visualization/"&gt;Building a tool showing how Gemini Pro can return bounding boxes for objects in images&lt;/a&gt;. I've now upgraded that tool to add support the new model.&lt;/p&gt;
&lt;p&gt;You can access it at &lt;a href="https://tools.simonwillison.net/gemini-bbox"&gt;tools.simonwillison.net/gemini-bbox&lt;/a&gt; - you'll need to provide your own Gemini API key which is sent directly to their API from your browser (it won't be logged by an intermediary).&lt;/p&gt;
&lt;p&gt;I tried it out on &lt;a href="https://static.simonwillison.net/static/2025/challenging-pelicans.jpg"&gt;a challenging photograph&lt;/a&gt; of some pelicans... and it worked extremely well:&lt;/p&gt;
&lt;p&gt;My prompt was:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Return bounding boxes around pelicans as JSON arrays [ymin, xmin, ymax, xmax]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Gemini models are all trained to return bounding boxes scaled between 0 and 100. My tool knows how to convert those back to the same dimensions as the input image.&lt;/p&gt;
&lt;p&gt;Here's what the visualized result looked like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/gemini-pelicans-bbox.jpg" alt="Pelicans with bounding boxes around them" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;It got almost all of them! I like how it didn't draw a box around the one egret that had made it into the photo.&lt;/p&gt;
&lt;h4 id="characteristics"&gt;More characteristics of the model&lt;/h4&gt;

&lt;p&gt;Here's the &lt;a href="https://ai.google.dev/gemini-api/docs/models#gemini-2.5-pro-exp-03-25"&gt;official model listing&lt;/a&gt; in the Gemini docs. Key details:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Input token limit: 1,000,000&lt;/li&gt;
  &lt;li&gt;Output token limit: 64,000 - this is a &lt;em&gt;huge&lt;/em&gt; upgrade, all of the other listed models have 8,192 for this (correction: &lt;a href="https://deepmind.google/technologies/gemini/flash-thinking/"&gt;Gemini 2.0 Flash Thinking&lt;/a&gt; also had a 64,000 output length)&lt;/li&gt;
  &lt;li&gt;Knowledge cut-off: January 2025 - an improvement on Gemini 2.0's August 2024&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="gemini-2-5-pro-is-a-very-strong-new-model"&gt;Gemini 2.5 Pro is a very strong new model&lt;/h4&gt;
&lt;p&gt;I've hardly scratched the surface when it comes to trying out Gemini 2.5 Pro so far. How's its creative writing? Factual knowledge about the world? Can it write great code in Python, JavaScript, Rust and more?&lt;/p&gt;
&lt;p&gt;The Gemini family of models have capabilities that set them apart from other models:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Long context length - Gemini 2.5 Pro supports up to 1 million tokens&lt;/li&gt;
&lt;li&gt;Audio input - something which few other models support, certainly not at this length and with this level of timestamp accuracy&lt;/li&gt;
&lt;li&gt;Accurate bounding box detection for image inputs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My experiments so far with these capabilities indicate that Gemini 2.5 Pro really is a very strong new model. I'm looking forward to exploring more of what it can do.&lt;/p&gt;

&lt;h4 id="update-it-s-very-good-at-code"&gt;Update: it's very good at code&lt;/h4&gt;
&lt;p&gt;I spent this evening trying it out for coding tasks, and it's very, very impressive. I'm seeing results for Python that feel comparable to my previous favourite Claude 3.7 Sonnet, and appear to be benefitting from Gemini 2.5 Pro's default reasoning mode and long context.&lt;/p&gt;
&lt;p&gt;I've been wanting to add a new content type of "notes" to my blog for quite a while now, but I was put off by the anticipated tedium of all of the different places in the codebase that would need to be updated.&lt;/p&gt;
&lt;p&gt;That feature is now live. &lt;a href="https://simonwillison.net/2025/Mar/26/notes/"&gt;Here are my notes&lt;/a&gt; on creating that notes feature using Gemini 2.5 Pro. It crunched through my entire codebase and figured out all of the places I needed to change - 18 files in total, as you can see in &lt;a href="https://github.com/simonw/simonwillisonblog/pull/527/files"&gt;the resulting PR&lt;/a&gt;. The whole project took about 45 minutes from start to finish - averaging less than three minutes per file I had to modify.&lt;/p&gt;
&lt;p&gt;I've thrown a whole bunch of other coding challenges at it, and the bottleneck on evaluating them has become my own mental capacity to review the resulting code!&lt;/p&gt;
&lt;p&gt;Here's another, more complex example. This hasn't resulted in actual running code yet but it took a big bite out of an architectural design problem I've been stewing on for a very long time.&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://llm.datasette.io/"&gt;LLM project&lt;/a&gt; needs support for tools - a way to teach different LLMs how to request tool execution, then have those tools (implemented in Python) run and return their results back to the models.&lt;/p&gt;

&lt;p&gt;Designing this is really hard, because I need to create an abstraction that works across multiple different model providers, each powered by a different plugin.&lt;/p&gt;

&lt;p&gt;Could Gemini 2.5 Pro help unblock me by proposing an architectural approach that might work?&lt;/p&gt;

&lt;p&gt;I started by combining the Python and Markdown files for my &lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;, &lt;a href="https://github.com/simonw/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://github.com/simonw/llm-gemini"&gt;llm-gemini&lt;/a&gt;, and &lt;a href="https://github.com/simonw/llm-anthropic"&gt;llm-anthropic&lt;/a&gt; repositories into one big document:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt sqlite-utils llm llm-gemini llm-anthropic -e md -e py -c&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Piping it through &lt;a href="https://github.com/simonw/ttok"&gt;ttok&lt;/a&gt; showed that to be 316,098 tokens (using the OpenAI tokenizer, but the Gemini tokenizer is likely a similar number).&lt;/p&gt;
&lt;p&gt;Then I fed that all into Gemini 2.5 Pro with the following prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Provide a detailed design proposal for adding tool calling support to LLM&lt;/p&gt;
&lt;p&gt;Different model plugins will have different ways of executing tools, but LLM itself should provide both a Python and a CLI abstraction over these differences.&lt;/p&gt;
&lt;p&gt;Tool calling involves passing tool definitions to a model, the model then replies with tools it would like executed, the harness code then executes those and passes the result back to the model and continues in a loop until the model stops requesting tools. This should happen as part of the existing llm.Conversation mechanism.&lt;/p&gt;
&lt;p&gt;At the Python layer tools should be provided as Python functions that use type hints, which can then be converted into JSON schema using Pydantic - similar to how the existing schema= mechanism works.&lt;/p&gt;
&lt;p&gt;For the CLI option tools will be provided as an option passed to the command, similar to how the sqlite-utils convert CLI mechanism works.'&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've been thinking about this problem for over a year now. &lt;a href="https://gist.github.com/simonw/481f46a779f20d8650b4fc5854f070c5"&gt;Gemini 2.5 Pro's response&lt;/a&gt; isn't exactly what I'm going to do, but it did include a flurry of genuinely useful suggestions to help me craft my final approach.&lt;/p&gt;
&lt;p&gt;I hadn't thought about the need for asyncio support for tool functions at all, so this idea from Gemini 2.5 Pro was very welcome:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;AsyncConversation.prompt()&lt;/code&gt; method will mirror the synchronous API, accepting the &lt;code&gt;tools&lt;/code&gt; parameter and handling the tool calling loop using &lt;code&gt;async/await&lt;/code&gt; for tool execution if the tool functions themselves are async. If tool functions are synchronous, they will be run in a thread pool executor via &lt;code&gt;asyncio.to_thread&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Watching Gemini crunch through some of the more complex problems I've been dragging my heels on reminded me of something Harper Reed wrote &lt;a href="https://harper.blog/2025/02/16/my-llm-codegen-workflow-atm/"&gt;about his   workflow with LLMs for code&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;My hack to-do list is empty because I built everything. I keep thinking of new things and knocking them out while watching a movie or something.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've been stressing quite a bit about my backlog of incomplete projects recently. I don't think Gemini 2.5 Pro is quite going to work through all of them while I'm watching TV, but it does feel like it's going to help make a significant dent in them.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/google"&gt;google&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/gemini"&gt;gemini&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vision-llms"&gt;vision-llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pelican-riding-a-bicycle"&gt;pelican-riding-a-bicycle&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-reasoning"&gt;llm-reasoning&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-release"&gt;llm-release&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chatbot-arena"&gt;chatbot-arena&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="google"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="gemini"/><category term="vision-llms"/><category term="pelican-riding-a-bicycle"/><category term="llm-reasoning"/><category term="llm-release"/><category term="files-to-prompt"/><category term="chatbot-arena"/></entry><entry><title>microsoft/playwright-mcp</title><link href="https://simonwillison.net/2025/Mar/25/playwright-mcp/#atom-tag" rel="alternate"/><published>2025-03-25T01:40:05+00:00</published><updated>2025-03-25T01:40:05+00:00</updated><id>https://simonwillison.net/2025/Mar/25/playwright-mcp/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/microsoft/playwright-mcp"&gt;microsoft/playwright-mcp&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The Playwright team at Microsoft have released an MCP (&lt;a href="https://github.com/microsoft/playwright-mcp"&gt;Model Context Protocol&lt;/a&gt;) server wrapping Playwright, and it's pretty fascinating.&lt;/p&gt;
&lt;p&gt;They implemented it on top of the Chrome accessibility tree, so MCP clients (such as the Claude Desktop app) can use it to drive an automated browser and use the accessibility tree to read and navigate pages that they visit.&lt;/p&gt;
&lt;p&gt;Trying it out is quite easy if you have Claude Desktop and Node.js installed already. Edit your &lt;code&gt;claude_desktop_config.json&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code ~/Library/Application\ Support/Claude/claude_desktop_config.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And add this:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
  &lt;span class="pl-ent"&gt;"mcpServers"&lt;/span&gt;: {
    &lt;span class="pl-ent"&gt;"playwright"&lt;/span&gt;: {
      &lt;span class="pl-ent"&gt;"command"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;npx&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-ent"&gt;"args"&lt;/span&gt;: [
        &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;@playwright/mcp@latest&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
      ]
    }
  }
}&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now when you launch Claude Desktop various new browser automation tools will be available to it, and you can tell Claude to navigate to a website and interact with it.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of Claude interface showing a conversation about Datasette. The interface shows Claude responding to a user (SW) after navigating to datasette.io. Claude's response includes page details (URL: https://datasette.io/, Title: Datasette: An open source multi-tool for exploring and publishing data) and a summary of what's visible on the site: a description of Datasette as an open-source tool for exploring and publishing data, the tagline &amp;quot;Find stories in data&amp;quot;, navigation options, and features including exploratory data analysis, instant data publishing, and rapid prototyping." src="https://static.simonwillison.net/static/2025/claude-playwright.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;I ran the following to get a list of the available tools:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /tmp
git clone https://github.com/microsoft/playwright-mcp
cd playwright-mcp/src/tools
files-to-prompt . | llm -m claude-3.7-sonnet \
  'Output a detailed description of these tools'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href="https://gist.github.com/simonw/69200999149221c549c1f62e7befa20f"&gt;full output is here&lt;/a&gt;, but here's the truncated tool list:&lt;/p&gt;
&lt;blockquote&gt;
&lt;h4&gt;Navigation Tools (&lt;code&gt;common.ts&lt;/code&gt;)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;browser_navigate&lt;/strong&gt;: Navigate to a specific URL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_go_back&lt;/strong&gt;: Navigate back in browser history&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_go_forward&lt;/strong&gt;: Navigate forward in browser history&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_wait&lt;/strong&gt;: Wait for a specified time in seconds&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_press_key&lt;/strong&gt;: Press a keyboard key&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_save_as_pdf&lt;/strong&gt;: Save current page as PDF&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_close&lt;/strong&gt;: Close the current page&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Screenshot and Mouse Tools (&lt;code&gt;screenshot.ts&lt;/code&gt;)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;browser_screenshot&lt;/strong&gt;: Take a screenshot of the current page&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_move_mouse&lt;/strong&gt;: Move mouse to specific coordinates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_click&lt;/strong&gt; (coordinate-based): Click at specific x,y coordinates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_drag&lt;/strong&gt; (coordinate-based): Drag mouse from one position to another&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_type&lt;/strong&gt; (keyboard): Type text and optionally submit&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Accessibility Snapshot Tools (&lt;code&gt;snapshot.ts&lt;/code&gt;)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;browser_snapshot&lt;/strong&gt;: Capture accessibility structure of the page&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_click&lt;/strong&gt; (element-based): Click on a specific element using accessibility reference&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_drag&lt;/strong&gt; (element-based): Drag between two elements&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_hover&lt;/strong&gt;: Hover over an element&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;browser_type&lt;/strong&gt; (element-based): Type text into a specific element&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&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/playwright"&gt;playwright&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/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/llm-tool-use"&gt;llm-tool-use&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/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai"/><category term="playwright"/><category term="generative-ai"/><category term="llms"/><category term="anthropic"/><category term="claude"/><category term="llm-tool-use"/><category term="model-context-protocol"/><category term="files-to-prompt"/></entry><entry><title>Here's how I use LLMs to help me write code</title><link href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#atom-tag" rel="alternate"/><published>2025-03-11T14:09:03+00:00</published><updated>2025-03-11T14:09:03+00:00</updated><id>https://simonwillison.net/2025/Mar/11/using-llms-for-code/#atom-tag</id><summary type="html">
    &lt;p&gt;Online discussions about &lt;a href="https://simonwillison.net/tags/ai-assisted-programming/"&gt;using Large Language Models to help write code&lt;/a&gt; inevitably produce comments from developers who's experiences have been disappointing. They often ask what they're doing wrong - how come some people are reporting such great results when their own experiments have proved lacking?&lt;/p&gt;
&lt;p&gt;Using LLMs to write code is &lt;strong&gt;difficult&lt;/strong&gt; and &lt;strong&gt;unintuitive&lt;/strong&gt;. It takes significant effort to figure out the sharp and soft edges of using them in this way, and there's precious little guidance to help people figure out how best to apply them.&lt;/p&gt;
&lt;p&gt;If someone tells you that coding with LLMs is &lt;em&gt;easy&lt;/em&gt; they are (probably unintentionally) misleading you. They may well have stumbled on to patterns that work, but those patterns do not come naturally to everyone.&lt;/p&gt;
&lt;p&gt;I've been getting great results out of LLMs for code for over two years now. Here's my attempt at transferring some of that experience and intution to you.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#set-reasonable-expectations"&gt;Set reasonable expectations&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#account-for-training-cut-off-dates"&gt;Account for training cut-off dates&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#context-is-king"&gt;Context is king&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#ask-them-for-options"&gt;Ask them for options&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#tell-them-exactly-what-to-do"&gt;Tell them exactly what to do&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#you-have-to-test-what-it-writes-"&gt;You have to test what it writes!&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#remember-it-s-a-conversation"&gt;Remember it's a conversation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#use-tools-that-can-run-the-code-for-you"&gt;Use tools that can run the code for you&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#vibe-coding-is-a-great-way-to-learn"&gt;Vibe-coding is a great way to learn&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#a-detailed-example"&gt;A detailed example using Claude Code&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#be-ready-for-the-human-to-take-over"&gt;Be ready for the human to take over&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#the-biggest-advantage-is-speed-of-development"&gt;The biggest advantage is speed of development&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#llms-amplify-existing-expertise"&gt;LLMs amplify existing expertise&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Mar/11/using-llms-for-code/#bonus-answering-questions-about-codebases"&gt;Bonus: answering questions about codebases&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h4 id="set-reasonable-expectations"&gt;Set reasonable expectations&lt;/h4&gt;
&lt;p&gt;Ignore the "AGI" hype - LLMs are still fancy autocomplete. All they do is predict a sequence of tokens - but it turns out writing code is mostly about stringing tokens together in the right order, so they can be &lt;em&gt;extremely&lt;/em&gt; useful for this provided you point them in the right direction.&lt;/p&gt;
&lt;p&gt;If you assume that this technology will implement your project perfectly without you needing to exercise any of your own skill you'll quickly be disappointed.&lt;/p&gt;
&lt;p&gt;Instead, use them to &lt;em&gt;augment&lt;/em&gt; your abilities. My current favorite mental model is to think of them as an over-confident pair programming assistant who's lightning fast at looking things up, can churn out relevant examples at a moment's notice and can execute on tedious tasks without complaint.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Over-confident&lt;/strong&gt; is important. They'll absolutely make mistakes - sometimes subtle, sometimes huge. These mistakes can be &lt;a href="https://simonwillison.net/2025/Mar/2/kellan-elliott-mccrea/"&gt;deeply inhuman&lt;/a&gt; - if a human collaborator hallucinated a non-existent library or method you would instantly lose trust in them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't fall into the trap of anthropomorphizing LLMs and assuming that failures which would discredit a human should discredit the machine in the same way.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When working with LLMs you'll often find things that they just cannot do. Make a note of these - they are useful lessons! They're also valuable examples to stash away for the future - a sign of a strong new model is when it produces usable results for a task that previous models had been unable to handle.&lt;/p&gt;
&lt;h4 id="account-for-training-cut-off-dates"&gt;Account for training cut-off dates&lt;/h4&gt;
&lt;p&gt;A crucial characteristic of any model is its &lt;strong&gt;training cut-off date&lt;/strong&gt;. This is the date at which the data they were trained on stopped being collected. For OpenAI's models this is usually October 2023 or May 2024. Other providers may have more recent dates.&lt;/p&gt;
&lt;p&gt;This is &lt;em&gt;extremely&lt;/em&gt; important for code, because it influences what libraries they will be familiar with. If the library you are using had a major breaking change since October 2023, some OpenAI models won't know about it!&lt;/p&gt;
&lt;p&gt;I gain enough value from LLMs that I now deliberately consider this when picking a library - I try to stick with libraries with good stability and that are popular enough that many examples of them will have made it into the training data. I like applying the principles of &lt;a href="https://boringtechnology.club/"&gt;boring technology&lt;/a&gt; - innovate on your project's unique selling points, stick with tried and tested solutions for everything else.&lt;/p&gt;
&lt;p&gt;LLMs can still help you work with libraries that exist outside their training data, but you need to put in more work - you'll need to feed them recent examples of how those libraries should be used as part of your prompt.&lt;/p&gt;
&lt;p&gt;This brings us to the most important thing to understand when working with LLMs:&lt;/p&gt;
&lt;h4 id="context-is-king"&gt;Context is king&lt;/h4&gt;
&lt;p&gt;Most of the craft of getting good results out of an LLM comes down to managing its context - the text that is part of your current conversation.&lt;/p&gt;
&lt;p&gt;This context isn't just the prompt that you have fed it: successful LLM interactions usually take the form of conversations, and the context consists of every message from you &lt;em&gt;and&lt;/em&gt; every reply from the LLM that exist in the current conversation thread.&lt;/p&gt;
&lt;p&gt;When you start a new conversation you reset that context back to zero. This is important to know, as often the fix for a conversation that has stopped being useful is to wipe the slate clean and start again.&lt;/p&gt;
&lt;p&gt;Some LLM coding tools go beyond just the conversation. Claude Projects for example allow you to pre-populate the context with quite a large amount of text - including a recent ability to &lt;a href="https://support.anthropic.com/en/articles/10167454-using-the-github-integration"&gt;import code directly from a GitHub&lt;/a&gt; repository which I'm using a &lt;em&gt;lot&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Tools like Cursor and VS Code Copilot include context from your current editor session and file layout automatically, and you can sometimes use mechanisms like &lt;a href="https://docs.cursor.com/context/@-symbols/overview"&gt;Cursor's @commands&lt;/a&gt; to pull in additional files or documentation.&lt;/p&gt;
&lt;p&gt;One of the reasons I mostly work directly with the &lt;a href="https://chatgpt.com/"&gt;ChatGPT&lt;/a&gt; and &lt;a href="https://claude.ai/"&gt;Claude&lt;/a&gt; web or app interfaces is that it makes it easier for me to understand exactly what is going into the context. LLM tools that obscure that context from me are &lt;em&gt;less&lt;/em&gt; effective.&lt;/p&gt;
&lt;p&gt;You can use the fact that previous replies are also part of the context to your advantage. For complex coding tasks try getting the LLM to write a simpler version first, check that it works and then iterate on building to the more sophisticated implementation.&lt;/p&gt;
&lt;p&gt;I often start a new chat by dumping in existing code to seed that context, then work with the LLM to modify it in some way.&lt;/p&gt;
&lt;p&gt;One of my favorite code prompting techniques is to drop in several full examples relating to something I want to build, then prompt the LLM to use them as inspiration for a new project. I wrote about that in detail when I &lt;a href="https://simonwillison.net/2024/Mar/30/ocr-pdfs-images/"&gt;described my JavaScript OCR application&lt;/a&gt; that combines Tesseract.js and PDF.js - two libraries I had used in the past and for which I could provide working examples in the prompt.&lt;/p&gt;
&lt;h4 id="ask-them-for-options"&gt;Ask them for options&lt;/h4&gt;
&lt;p&gt;Most of my projects start with some open questions: is the thing I'm trying to do possible? What are the potential ways I could implement it? Which of those options are the &lt;em&gt;best&lt;/em&gt;?&lt;/p&gt;
&lt;p&gt;I use LLMs as part of this initial research phase.&lt;/p&gt;
&lt;p&gt;I'll use prompts like "what are options for HTTP libraries in Rust? Include usage examples" - or "what are some useful drag-and-drop libraries in JavaScript? Build me an artifact demonstrating each one" (to Claude).&lt;/p&gt;
&lt;p&gt;The training cut-off is relevant here, since it means newer libraries won't be suggested. Usually that's OK - I don't want the latest, I want the most stable and the one that has been around for long enough for the bugs to be ironed out.&lt;/p&gt;
&lt;p&gt;If I'm going to use something more recent I'll do that research myself, outside of LLM world.&lt;/p&gt;
&lt;p&gt;The best way to start any project is with a prototype that proves that the key requirements of that project can be met. I often find that an LLM can get me to that working prototype within a few minutes of me sitting down with my laptop - or sometimes even while working on my phone.&lt;/p&gt;
&lt;h4 id="tell-them-exactly-what-to-do"&gt;Tell them exactly what to do&lt;/h4&gt;
&lt;p&gt;Once I've completed the initial research I change modes dramatically. For production code my LLM usage is much more authoritarian: I treat it like a digital intern, hired to type code for me based on my detailed instructions.&lt;/p&gt;
&lt;p&gt;Here's a recent example:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Write a Python function that uses asyncio httpx with this signature:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def download_db(url, max_size_bytes=5 * 1025 * 1025): -&amp;gt; pathlib.Path
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Given a URL, this downloads the database to a temp directory and returns a path to it. BUT it checks the content length header at the start of streaming back that data and, if it's more than the limit, raises an error. When the download finishes it uses &lt;code&gt;sqlite3.connect(...)&lt;/code&gt; and then runs a &lt;code&gt;PRAGMA quick_check&lt;/code&gt; to confirm the SQLite data is valid - raising an error if not. Finally, if the content length header lies to us -  if it says 2MB but we download 3MB - we get an error raised as soon as we notice that problem.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I could write this function myself, but it would take me the better part of fifteen minutes to look up all of the details and get the code working right. Claude knocked it out &lt;a href="https://gist.github.com/simonw/5aed8bd87016c77465c23e0dc4563ec9"&gt;in 15 seconds&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I find LLMs respond extremely well to function signatures like the one I use here. I get to act as the function designer, the LLM does the work of building the body to my specification.&lt;/p&gt;
&lt;p&gt;I'll often follow-up with "Now write me the tests using pytest". Again, I dictate my technology of choice - I want the LLM to save me the time of having to type out the code that's sitting in my head already.&lt;/p&gt;
&lt;p&gt;If your reaction to this is "surely typing out the code is faster than typing out an English instruction of it", all I can tell you is that it really isn't for me any more. Code needs to be correct. English has enormous room for shortcuts, and vagaries, and typos, and saying things like "use that popular HTTP library" if you can't remember the name off the top of your head.&lt;/p&gt;
&lt;p&gt;The good coding LLMs are excellent at filling in the gaps. They're also much less lazy than me - they'll remember to catch likely exceptions, add accurate docstrings, and annotate code with the relevant types.&lt;/p&gt;
&lt;h4 id="you-have-to-test-what-it-writes-"&gt;You have to test what it writes!&lt;/h4&gt;
&lt;p&gt;I wrote about this &lt;a href="https://simonwillison.net/2025/Mar/2/hallucinations-in-code/#qa"&gt;at length last week&lt;/a&gt;: the one thing you absolutely cannot outsource to the machine is testing that the code actually works.&lt;/p&gt;
&lt;p&gt;Your responsibility as a software developer is to deliver working systems. If you haven't seen it run, it's not a working system. You need to invest in strengthening those manual QA habits.&lt;/p&gt;
&lt;p&gt;This may not be glamorous but it's always been a critical part of shipping good code, with or without the involvement of LLMs.&lt;/p&gt;
&lt;h4 id="remember-it-s-a-conversation"&gt;Remember it's a conversation&lt;/h4&gt;
&lt;p&gt;If I don't like what an LLM has written, they'll &lt;em&gt;never&lt;/em&gt; complain at being told to refactor it! "Break that repetitive code out into a function", "use string manipulation methods rather than a regular expression", or even "write that better!" - the code an LLM produces first time is rarely the final implementation, but they can re-type it dozens of times for you without ever getting frustrated or bored.&lt;/p&gt;
&lt;p&gt;Occasionally I'll get a great result from my first prompt - more frequently the more I practice - but I expect to need at least a few follow-ups.&lt;/p&gt;
&lt;p&gt;I often wonder if this is one of the key tricks that people are missing - a bad initial result isn't a failure, it's a starting point for pushing the model in the direction of the thing you actually want.&lt;/p&gt;
&lt;h4 id="use-tools-that-can-run-the-code-for-you"&gt;Use tools that can run the code for you&lt;/h4&gt;
&lt;p&gt;An increasing number of LLM coding tools now have the ability to &lt;em&gt;run that code&lt;/em&gt; for you. I'm slightly cautious about some of these since there's a possibility of the wrong command causing real damage, so I tend to stick to the ones that run code in a safe sandbox. My favorites right now are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT Code Interpreter&lt;/strong&gt;, where ChatGPT can write and then execute Python code directly in a Kubernetes sandbox VM managed by OpenAI. This is completely safe - it can't even make outbound network connections so really all that can happen is the temporary filesystem gets mangled and then reset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Artifacts&lt;/strong&gt;, where Claude can build you a full HTML+JavaScript+CSS web application that is displayed within the Claude interface. This web app is displayed in a &lt;em&gt;very&lt;/em&gt; locked down iframe sandbox, greatly restricting what it can do but preventing problems like accidental exfiltration of your private Claude data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT Canvas&lt;/strong&gt; is a newer ChatGPT feature with similar capabilites to Claude Artifacts. I have not explored this enough myself yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And if you're willing to live a little more dangerously:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.cursor.com/"&gt;Cursor&lt;/a&gt;&lt;/strong&gt; has an "Agent" feature that can do this, as does &lt;strong&gt;&lt;a href="https://codeium.com/windsurf"&gt;Windsurf&lt;/a&gt;&lt;/strong&gt; and a growing number of other editors. I haven't spent enough time with these to make recommendations yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://aider.chat/"&gt;Aider&lt;/a&gt;&lt;/strong&gt; is the leading open source implementation of these kinds of patterns, and is a great example of &lt;a href="https://en.wikipedia.org/wiki/Eating_your_own_dog_food"&gt;dogfooding&lt;/a&gt; - recent releases of Aider have been &lt;a href="https://aider.chat/HISTORY.html"&gt;80%+ written&lt;/a&gt; by Aider itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview"&gt;Claude Code&lt;/a&gt;&lt;/strong&gt; is Anthropic's new entrant into this space. I'll provide a detailed description of using that tool shortly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This run-the-code-in-a-loop pattern is so powerful that I chose my core LLM tools for coding based primarily on whether they can safely run and iterate on my code.&lt;/p&gt;
&lt;h4 id="vibe-coding-is-a-great-way-to-learn"&gt;Vibe-coding is a great way to learn&lt;/h4&gt;
&lt;p&gt;Andrej Karpathy &lt;a href="https://simonwillison.net/2025/Feb/6/andrej-karpathy/"&gt;coined the term&lt;/a&gt; vibe-coding just over a month ago, and it has stuck:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There's a new kind of coding I call "vibe coding", where you fully give in to the vibes, embrace exponentials, and forget that the code even exists. [...] I ask for the dumbest things like "decrease the padding on the sidebar by half" because I'm too lazy to find it. I "Accept All" always, I don't read the diffs anymore. When I get error messages I just copy paste them in with no comment, usually that fixes it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Andrej suggests this is "not too bad for throwaway weekend projects". It's also a &lt;em&gt;fantastic&lt;/em&gt; way to explore the capabilities of these models - and really fun.&lt;/p&gt;
&lt;p&gt;The best way to learn LLMs is to play with them. Throwing absurd ideas at them and vibe-coding until they almost sort-of work is a genuinely useful way to accelerate the rate at which you build intuition for what works and what doesn't.&lt;/p&gt;
&lt;p&gt;I've been vibe-coding since before Andrej gave it a name! My &lt;a href="https://github.com/simonw/tools"&gt;simonw/tools&lt;/a&gt; GitHub repository has 77 HTML+JavaScript apps and 6 Python apps, and every single one of them was built by prompting LLMs. I have learned &lt;em&gt;so much&lt;/em&gt; from building this collection, and I add to it at a rate of several new prototypes per week.&lt;/p&gt;
&lt;p&gt;You can try most of mine out directly on &lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt; - a GitHub Pages published version of the repo. I wrote more detailed notes on some of these back in October in &lt;a href="https://simonwillison.net/2024/Oct/21/claude-artifacts/"&gt;Everything I built with Claude Artifacts this week&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you want to see the transcript of the chat used for each one it's almost always linked to in the commit history for that page - or visit the new &lt;a href="https://tools.simonwillison.net/colophon"&gt;colophon page&lt;/a&gt; for an index that includes all of those links.&lt;/p&gt;
&lt;h4 id="a-detailed-example"&gt;A detailed example using Claude Code&lt;/h4&gt;
&lt;p&gt;While I was writing this article I had the idea for that &lt;a href="https://tools.simonwillison.net/colophon"&gt;tools.simonwillison.net/colophon&lt;/a&gt; page - I wanted something I could link to that showed the commit history of each of my tools in a more obvious way than GitHub.&lt;/p&gt;
&lt;p&gt;I decided to use that as an opportunity to demonstrate my AI-assisted coding process.&lt;/p&gt;
&lt;p&gt;For this one I used &lt;a href="https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview"&gt;Claude Code&lt;/a&gt;, because I wanted it to be able to run Python code directly against my existing tools repository on my laptop.&lt;/p&gt;
&lt;p&gt;Running the &lt;code&gt;/cost&lt;/code&gt; command at the end of my session showed me this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; /cost 
  ⎿  Total cost: $0.61
     Total duration (API): 5m 31.2s
     Total duration (wall): 17m 18.7s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The initial project took me just over 17 minutes from start to finish, and cost me 61 cents in API calls to Anthropic.&lt;/p&gt;
&lt;p&gt;I used the authoritarian process where I told the model exactly what I wanted to build. Here's my sequence of prompts (&lt;a href="https://gist.github.com/simonw/323e1b00ee4f8453c7834a7560eeafc1"&gt;full transcript here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I started by asking for an initial script to gather the data needed for the new page:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Almost all of the HTML files in this directory were created using Claude prompts, and the details of those prompts are linked in the commit messages. Build a Python script that checks the commit history for each HTML file in turn and extracts any URLs from those commit messages into a list. It should then output a JSON file with this structure: {"pages": {"name-of-file.html": ["url"], {"name-of-file-2.html": ["url1", "url2"], ... - as you can see, some files may have more than one URL in their commit history. The script should be called gather_links.py and it should save a JSON file called gathered_links.json&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I really didn't think very hard about this first prompt - it was more of a stream of consciousness that I typed into the bot as I thought about the initial problem.&lt;/p&gt;
&lt;p&gt;I inspected the initial result and spotted some problems:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It looks like it just got the start of the URLs, it should be getting the whole URLs which might be to different websites - so just get anything that starts https:// and ends with whitespace or the end of the commit message&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then I changed my mind - I wanted those full commit messages too:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Update the script - I want to capture the full commit messages AND the URLs - the new format should be {"pages": {"aria-live-regions.html": {"commits": [{"hash": hash, "message": message, "date": iso formatted date], "urls": [list of URLs like before]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Providing examples like this is a great shortcut to getting exactly what you want.&lt;/p&gt;
&lt;p&gt;Note that at no point have I looked at the code it's written in &lt;a href="https://github.com/simonw/tools/blob/87e2577983f11fc9c7bf7b7a268cf2404a21e1c5/gather_links.py"&gt;gather_links.py&lt;/a&gt;! This is pure vibe-coding: I'm looking at what it's doing, but I've left the implementation details entirely up to the LLM.&lt;/p&gt;
&lt;p&gt;The JSON looked good to me, so I said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is working great. Write me a new script called build_colophon.py which looks through that gathered JSON file and builds and saves an HTML page. The page should be mobile friendly and should list every page - with a link to that page - and for each one display the commit messages neatly (convert newlines to br and linkify URLs but no other formatting) - plus the commit message dates and links to the commits themselves which are in &lt;a href="https://github.com/simonw/tools"&gt;https://github.com/simonw/tools&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Claude knows how GitHub URLs works, so telling it to link to the commits and providing the repo name was enough for it guess &lt;code&gt;https://github.com/simonw/tools/commit/fd9daf885c924ba277806b3440457d52b0ad90a8&lt;/code&gt; for those commit URLs.&lt;/p&gt;
&lt;p&gt;I tend to find Claude has good default taste when it comes to web page design - I said "the page should be mobile friendly" and left it at that.&lt;/p&gt;
&lt;p&gt;Claude churned away and built me a page that wasn't right, so I said:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;it's not working right. ocr.html had a bunch of commits but in colophon.html there is only one link and heading for the first commit and the rest are shown within that same block - there should be separate HTML chunks with links and formatted dates for each of the other commits. Also the neatly formatted date should include the HH:MM as well as the date&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It fixed the bug all on its own, leaving just two changes I decided to make:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;it's almost perfect, but each page should have the commits displayed in the opposite order - oldest first&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And then:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;One last change - the pages are currently listed alphabetically, lets instead list them with the most recently modified at the top&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And that was the entire project! Here's &lt;a href="https://github.com/simonw/tools/blob/1e04f12a1cacea8856946162457d0d77e60ee549/build_colophon.py"&gt;build_colophon.py&lt;/a&gt;, and the page it generated came out &lt;a href="https://static.simonwillison.net/static/2025/colophon.html"&gt;looking pretty good&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/colophon.jpg" alt="Tools Colophon. This page documents the creation of the tools on tools.simonwillison.net, including links to the Claude conversations used to build them. social-media-cropper.html b4a2bc December 10, 2024 20:35 Social media cropper  https://gist.github.com/simonw/12b8f88932a71450071190e1289a17e9 a10954 February 28, 2025 16:02 Support 2:1 and 14:10 ratios  https://gist.github.com/simonw/e23917eddcbb368c9b6180d581f8f40a" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;There was one remaining task: I needed to deploy the new colophon as part of my site, but I didn't want to check that new &lt;code&gt;colophon.html&lt;/code&gt; page into the repository itself. I wanted a custom GitHub Pages build process.&lt;/p&gt;
&lt;p&gt;I started a brand new Claude Code session (clearing the slate) to see if that was something Claude could figure out too:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This repo is deployed using GitHub Pages. I want to modify the build slightly - before deploying to GitHub Pages it should run python gather_links.py which will generate a file called gathered_links.json - then it should run python build_colophon.py which will use that file to create colophon.html - I want both gathered_links.json and colophon.html to be published to GitHub pages but I do not want them checked into the repo itself&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Unlike the first time, this time I watched what it was doing very closely - I don't know how to customize GitHub Pages builds in this way and I wanted to both learn how to do it and stay cautious that it might hallucinate details and fail at the task.&lt;/p&gt;
&lt;p&gt;It told me it wanted to create this file and asked for my permission:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml"&gt;&lt;pre&gt;&lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Deploy to GitHub Pages&lt;/span&gt;

&lt;span class="pl-ent"&gt;on&lt;/span&gt;:
  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Run on push to main branch&lt;/span&gt;
  &lt;span class="pl-ent"&gt;push&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;branches&lt;/span&gt;: &lt;span class="pl-s"&gt;[ main ]&lt;/span&gt;
  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Allow manual trigger&lt;/span&gt;
  &lt;span class="pl-ent"&gt;workflow_dispatch&lt;/span&gt;:

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Sets permissions of the GITHUB_TOKEN&lt;/span&gt;
&lt;span class="pl-ent"&gt;permissions&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;contents&lt;/span&gt;: &lt;span class="pl-s"&gt;read&lt;/span&gt;
  &lt;span class="pl-ent"&gt;pages&lt;/span&gt;: &lt;span class="pl-s"&gt;write&lt;/span&gt;
  &lt;span class="pl-ent"&gt;id-token&lt;/span&gt;: &lt;span class="pl-s"&gt;write&lt;/span&gt;

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Allow only one concurrent deployment&lt;/span&gt;
&lt;span class="pl-ent"&gt;concurrency&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;group&lt;/span&gt;: &lt;span class="pl-s"&gt;pages&lt;/span&gt;
  &lt;span class="pl-ent"&gt;cancel-in-progress&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;

&lt;span class="pl-ent"&gt;jobs&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;build&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;runs-on&lt;/span&gt;: &lt;span class="pl-s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="pl-ent"&gt;steps&lt;/span&gt;:
      - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Checkout&lt;/span&gt;
        &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="pl-ent"&gt;with&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;fetch-depth&lt;/span&gt;: &lt;span class="pl-c1"&gt;0&lt;/span&gt;  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Need full history for git log in gather_links.py&lt;/span&gt;

      - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Setup Python&lt;/span&gt;
        &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="pl-ent"&gt;with&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;python-version&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;3.12&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;

      - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="pl-ent"&gt;run&lt;/span&gt;: &lt;span class="pl-s"&gt;|&lt;/span&gt;
&lt;span class="pl-s"&gt;          python -m pip install --upgrade pip&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;      - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Generate links and build colophon&lt;/span&gt;
        &lt;span class="pl-ent"&gt;run&lt;/span&gt;: &lt;span class="pl-s"&gt;|&lt;/span&gt;
&lt;span class="pl-s"&gt;          python gather_links.py&lt;/span&gt;
&lt;span class="pl-s"&gt;          python build_colophon.py&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;      - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Setup Pages&lt;/span&gt;
        &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/configure-pages@v4&lt;/span&gt;

      - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Build with Jekyll&lt;/span&gt;
        &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/jekyll-build-pages@v1&lt;/span&gt;

      - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Upload artifact&lt;/span&gt;
        &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/upload-pages-artifact@v3&lt;/span&gt;

  &lt;span class="pl-ent"&gt;deploy&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;environment&lt;/span&gt;:
      &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;github-pages&lt;/span&gt;
      &lt;span class="pl-ent"&gt;url&lt;/span&gt;: &lt;span class="pl-s"&gt;${{ steps.deployment.outputs.page_url }}&lt;/span&gt;
    &lt;span class="pl-ent"&gt;runs-on&lt;/span&gt;: &lt;span class="pl-s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="pl-ent"&gt;needs&lt;/span&gt;: &lt;span class="pl-s"&gt;build&lt;/span&gt;
    &lt;span class="pl-ent"&gt;steps&lt;/span&gt;:
      - &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Deploy to GitHub Pages&lt;/span&gt;
        &lt;span class="pl-ent"&gt;id&lt;/span&gt;: &lt;span class="pl-s"&gt;deployment&lt;/span&gt;
        &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;actions/deploy-pages@v4&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I guess that's right? I appreciated that comment about "Need full history for git log in gather_links.py" - that's something I could easily have missed.&lt;/p&gt;
&lt;p&gt;Then it said it wanted to add those files to &lt;code&gt;.gitignore&lt;/code&gt; - sounds like a good idea.&lt;/p&gt;
&lt;p&gt;It spat out this summary of its actions so far:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I've created a GitHub Actions workflow for GitHub Pages that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creates a .github/workflows/pages.yml file that:
&lt;ul&gt;
&lt;li&gt;Runs on pushes to main branch&lt;/li&gt;
&lt;li&gt;Checkouts the full git history (needed for gather_links.py)&lt;/li&gt;
&lt;li&gt;Sets up Python 3.12&lt;/li&gt;
&lt;li&gt;Runs both scripts in sequence to generate the files&lt;/li&gt;
&lt;li&gt;Builds and deploys to GitHub Pages&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Created .gitignore file to prevent the generated files from being committed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Now when you push to main, GitHub Actions will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run the gather_links.py script to create gathered_links.json&lt;/li&gt;
&lt;li&gt;Run build_colophon.py to create colophon.html&lt;/li&gt;
&lt;li&gt;Deploy both files (and all other files) to GitHub Pages&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I checked to see how much it had cost:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; /cost 
  ⎿  Total cost: $0.1788
     Total duration (API): 44.6s
     Total duration (wall): 10m 18.6s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So 17 cents and 45 seconds using the Claude API. (I got distracted, hence the 10m of total time.) Here's the &lt;a href="https://gist.github.com/simonw/a560b07eef577e6183021d1ccaae7e07"&gt;full transcript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The code didn't look like it would irreversibly break anything, so I pushed it to GitHub to see what would happen.&lt;/p&gt;
&lt;p&gt;... and it worked! My new &lt;a href="https://tools.simonwillison.net/colophon"&gt;colophon page&lt;/a&gt; was live.&lt;/p&gt;
&lt;p&gt;There's a catch. I watched the &lt;a href="https://github.com/simonw/tools/actions"&gt;GitHub Actions&lt;/a&gt; interface while it was running and something didn't look right:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/github-actions-colophon.jpg" alt="GitHub Actions interface showing three completed actions. Test for Custom pages workflow for colophon,2 Deploy for that same name and another one called pages-build-deployment." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I was expecting that "Test" job, but why were there two separate deploys?&lt;/p&gt;
&lt;p&gt;I had a hunch that the previous, default Jekyll deploy was still running, while the new deploy ran at the same time - and it was pure luck of the timing that the new script finished later and over-wrote the result of the original.&lt;/p&gt;
&lt;p&gt;It was time to ditch the LLMs and read some documentation!&lt;/p&gt;
&lt;p&gt;I found this page on &lt;a href="https://docs.github.com/en/pages/getting-started-with-github-pages/using-custom-workflows-with-github-pages"&gt;Using custom workflows with GitHub Pages&lt;/a&gt; but it didn't tell me what I needed to know.&lt;/p&gt;
&lt;p&gt;On another hunch I checked the GitHub Pages settings interface for my repo and found this option:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/github-pages-settings.jpg" alt="GitHub Pages UI - shows your site is live at tools.simonwillison.net, deployed 7 minutes ago. - then under Buyld and deployment a source menu shows options for GitHub Actions or for Deploy from a branch (selected)" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;My repo was set to "Deploy from a branch", so I switched that over to "GitHub Actions".&lt;/p&gt;
&lt;p&gt;I manually updated my &lt;code&gt;README.md&lt;/code&gt; to add a link to the new Colophon page in &lt;a href="https://github.com/simonw/tools/commit/4ee15aaad8e9a412505210a30f485528cb3c0390"&gt;this commit&lt;/a&gt;, which triggered another build.&lt;/p&gt;
&lt;p&gt;This time only two jobs ran, and the end result was the correctly deployed site:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/github-actions-colophon-2.jpg" alt="Only two in-progress workflows now, one is the Test one and the other is the Deploy to GitHub Pages one." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;(I later spotted another bug - some of the links inadvertently included &lt;code&gt;&amp;lt;br&amp;gt;&lt;/code&gt; tags in their &lt;code&gt;href=&lt;/code&gt;, which I &lt;a href="https://github.com/simonw/tools/commit/87e2577983f11fc9c7bf7b7a268cf2404a21e1c5"&gt;fixed&lt;/a&gt; with another &lt;a href="https://gist.github.com/simonw/d5ccbca1b530868980609222790a97cb"&gt;11 cent Claude Code session&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: I improved the colophon further by &lt;a href="https://simonwillison.net/2025/Mar/13/tools-colophon/"&gt;adding AI-generated descriptions of the tools&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="be-ready-for-the-human-to-take-over"&gt;Be ready for the human to take over&lt;/h4&gt;
&lt;p&gt;I got lucky with this example because it helped illustrate my final point: expect to need to take over.&lt;/p&gt;
&lt;p&gt;LLMs are no replacement for human intuition and experience. I've spent enough time with GitHub Actions that I know what kind of things to look for, and in this case it was faster for me to step in and finish the project rather than keep on trying to get there with prompts.&lt;/p&gt;
&lt;h4 id="the-biggest-advantage-is-speed-of-development"&gt;The biggest advantage is speed of development&lt;/h4&gt;
&lt;p&gt;My new &lt;a href="https://tools.simonwillison.net/colophon"&gt;colophon page&lt;/a&gt; took me just under half an hour from conception to finished, deployed feature.&lt;/p&gt;
&lt;p&gt;I'm certain it would have taken me significantly longer without LLM assistance - to the point that I probably wouldn't have bothered to build it at all.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This&lt;/em&gt; is why I care so much about the productivity boost I get from LLMs so much: it's not about getting work done faster, it's about being able to ship projects that I wouldn't have been able to justify spending time on at all.&lt;/p&gt;
&lt;p&gt;I wrote about this in March 2023: &lt;a href="https://simonwillison.net/2023/Mar/27/ai-enhanced-development/"&gt;AI-enhanced development makes me more ambitious with my projects&lt;/a&gt;. Two years later that effect shows no sign of wearing off.&lt;/p&gt;
&lt;p&gt;It's also a great way to accelerate learning new things - today that was how to customize my GitHub Pages builds using Actions, which is something I'll certainly use again in the future.&lt;/p&gt;
&lt;p&gt;The fact that LLMs let me execute my ideas faster means I can implement more of them, which means I can learn even more.&lt;/p&gt;
&lt;h4 id="llms-amplify-existing-expertise"&gt;LLMs amplify existing expertise&lt;/h4&gt;
&lt;p&gt;Could anyone else have done this project in the same way? Probably not! My prompting here leaned on 25+ years of professional coding experience, including my previous explorations of GitHub Actions, GitHub Pages, GitHub itself and the LLM tools I put into play.&lt;/p&gt;
&lt;p&gt;I also &lt;em&gt;knew&lt;/em&gt; that this was going to work. I've spent enough time working with these tools that I was confident that assembling a new HTML page with information pulled from my Git history was entirely within the capabilities of a good LLM.&lt;/p&gt;
&lt;p&gt;My prompts reflected that - there was nothing particularly novel here, so I dictated the design, tested the results as it was working and occasionally nudged it to fix a bug.&lt;/p&gt;
&lt;p&gt;If I was trying to build a Linux kernel driver - a field I know virtually nothing about - my process would be entirely different.&lt;/p&gt;
&lt;h4 id="bonus-answering-questions-about-codebases"&gt;Bonus: answering questions about codebases&lt;/h4&gt;
&lt;p&gt;If the idea of using LLMs to write code for you still feels deeply unappealing, there's another use-case for them which you may find more compelling.&lt;/p&gt;
&lt;p&gt;Good LLMs are &lt;em&gt;great&lt;/em&gt; at answering questions about code.&lt;/p&gt;
&lt;p&gt;This is also very low stakes: the worst that can happen is they might get something wrong, which may take you a tiny bit longer to figure out. It's still likely to save you time compared to digging through thousands of lines of code entirely by yourself.&lt;/p&gt;
&lt;p&gt;The trick here is to dump the code into a long context model and start asking questions. My current favorite for this is the catchily titled &lt;code&gt;gemini-2.0-pro-exp-02-05&lt;/code&gt;, a preview of Google's Gemini 2.0 Pro which is currently free to use via their API.&lt;/p&gt;
&lt;p&gt;I used this trick just &lt;a href="https://simonwillison.net/2025/Mar/6/monolith/"&gt;the other day&lt;/a&gt;. I was trying out a new-to-me tool called &lt;a href="https://github.com/Y2Z/monolith"&gt;monolith&lt;/a&gt;, a CLI tool written in Rust which downloads a web page and all of its dependent assets (CSS, images etc) and bundles them together into a single archived file.&lt;/p&gt;
&lt;p&gt;I was curious as to how it worked, so I cloned it into my temporary directory and ran these commands:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;&lt;span class="pl-c1"&gt;cd&lt;/span&gt; /tmp
git clone https://github.com/Y2Z/monolith
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; monolith

files-to-prompt &lt;span class="pl-c1"&gt;.&lt;/span&gt; -c &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m gemini-2.0-pro-exp-02-05 \
  -s &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;architectural overview as markdown&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I'm using my own &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; tool (built for me by Claude 3 Opus &lt;a href="https://simonwillison.net/2024/Apr/8/files-to-prompt/"&gt;last year&lt;/a&gt;) here to gather the contents of all of the files in the repo into a single stream. Then I pipe that into my &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; tool and tell it (via the &lt;a href="https://github.com/simonw/llm-gemini"&gt;llm-gemini&lt;/a&gt; plugin) to prompt Gemini 2.0 Pro with a system prompt of "architectural overview as markdown".&lt;/p&gt;
&lt;p&gt;This gave me back a &lt;a href="https://gist.github.com/simonw/2c80749935ae3339d6f7175dc7cf325b"&gt;detailed document&lt;/a&gt; describing how the tool works - which source files do what and, crucially, which Rust crates it was using. I learned that it used &lt;code&gt;reqwest&lt;/code&gt;, &lt;code&gt;html5ever&lt;/code&gt;, &lt;code&gt;markup5ever_rcdom&lt;/code&gt; and &lt;code&gt;cssparser&lt;/code&gt; and that it doesn't evaluate JavaScript at all, an important limitation.&lt;/p&gt;
&lt;p&gt;I use this trick several times a week. It's a great way to start diving into a new codebase - and often the alternative isn't spending more time on this, it's failing to satisfy my curiosity at all.&lt;/p&gt;
&lt;p&gt;I included three more examples in &lt;a href="https://simonwillison.net/2025/Feb/14/files-to-prompt/"&gt;this recent post&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&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/gemini"&gt;gemini&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/vibe-coding"&gt;vibe-coding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-to-app"&gt;prompt-to-app&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="tools"/><category term="ai"/><category term="github-actions"/><category term="openai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="anthropic"/><category term="claude"/><category term="gemini"/><category term="claude-artifacts"/><category term="vibe-coding"/><category term="files-to-prompt"/><category term="coding-agents"/><category term="claude-code"/><category term="prompt-to-app"/></entry><entry><title>monolith</title><link href="https://simonwillison.net/2025/Mar/6/monolith/#atom-tag" rel="alternate"/><published>2025-03-06T15:37:48+00:00</published><updated>2025-03-06T15:37:48+00:00</updated><id>https://simonwillison.net/2025/Mar/6/monolith/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Y2Z/monolith"&gt;monolith&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Neat CLI tool built in Rust that can create a single packaged HTML file of a web page plus all of its dependencies.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo install monolith # or brew install
monolith https://simonwillison.net/ &amp;gt; simonwillison.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That command produced &lt;a href="https://static.simonwillison.net/static/2025/simonwillison.html"&gt;this 1.5MB single file result&lt;/a&gt;. All of the linked images, CSS and JavaScript assets have had their contents inlined into base64 URIs in their &lt;code&gt;src=&lt;/code&gt; and &lt;code&gt;href=&lt;/code&gt; attributes.&lt;/p&gt;
&lt;p&gt;I was intrigued as to how it works, so I dumped the whole repository into Gemini 2.0 Pro and asked for an architectural summary:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /tmp
git clone https://github.com/Y2Z/monolith
cd monolith
files-to-prompt . -c | llm -m gemini-2.0-pro-exp-02-05 \
  -s 'architectural overview as markdown'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/2c80749935ae3339d6f7175dc7cf325b"&gt;what I got&lt;/a&gt;. Short version: it uses the &lt;code&gt;reqwest&lt;/code&gt;, &lt;code&gt;html5ever&lt;/code&gt;, &lt;code&gt;markup5ever_rcdom&lt;/code&gt; and &lt;code&gt;cssparser&lt;/code&gt; crates to fetch and parse HTML and CSS and extract, combine and rewrite the assets. It doesn't currently attempt to run any JavaScript.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=42933383#42935115"&gt;Comment on Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scraping"&gt;scraping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&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/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="cli"/><category term="scraping"/><category term="ai"/><category term="rust"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="files-to-prompt"/></entry><entry><title>files-to-prompt 0.6</title><link href="https://simonwillison.net/2025/Feb/19/files-to-prompt/#atom-tag" rel="alternate"/><published>2025-02-19T06:12:12+00:00</published><updated>2025-02-19T06:12:12+00:00</updated><id>https://simonwillison.net/2025/Feb/19/files-to-prompt/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/files-to-prompt/releases/tag/0.6"&gt;files-to-prompt 0.6&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New release of my CLI tool for turning a whole directory of code into a single prompt ready to pipe or paste into an LLM.&lt;/p&gt;
&lt;p&gt;Here are the full release notes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;ul&gt;&lt;li&gt;New &lt;code&gt;-m/--markdown&lt;/code&gt; option for outputting results as Markdown with each file in a fenced code block. &lt;a href="https://github.com/simonw/files-to-prompt/issues/42"&gt;#42&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Support for reading a list of files from standard input. Thanks, &lt;a href="https://github.com/thelastnode"&gt;Ankit Shankar&lt;/a&gt;. &lt;a href="https://github.com/simonw/files-to-prompt/issues/44"&gt;#44&lt;/a&gt;&lt;br&gt;
  Here's how to process just files modified within the last day:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -mtime -1 | files-to-prompt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also use the &lt;code&gt;-0/--null&lt;/code&gt; flag to accept lists of file paths separated by null delimiters, which is useful for handling file names with spaces in them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -name "*.txt" -print0 | files-to-prompt -0
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;I also have a potential fix for a reported bug concerning nested &lt;code&gt;.gitignore&lt;/code&gt; files that's currently &lt;a href="https://github.com/simonw/files-to-prompt/pull/45"&gt;sitting in a PR&lt;/a&gt;. I'm waiting for someone else to confirm that it behaves as they would expect. I've left &lt;a href="https://github.com/simonw/files-to-prompt/issues/40#issuecomment-2667571418"&gt;details in this issue comment&lt;/a&gt;, but the short version is that you can try out the version from the PR using this &lt;code&gt;uvx&lt;/code&gt; incantation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uvx --with git+https://github.com/simonw/files-to-prompt@nested-gitignore files-to-prompt
&lt;/code&gt;&lt;/pre&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="cli"/><category term="projects"/><category term="annotated-release-notes"/><category term="llms"/><category term="uv"/><category term="files-to-prompt"/></entry><entry><title>files-to-prompt 0.5</title><link href="https://simonwillison.net/2025/Feb/14/files-to-prompt/#atom-tag" rel="alternate"/><published>2025-02-14T04:14:21+00:00</published><updated>2025-02-14T04:14:21+00:00</updated><id>https://simonwillison.net/2025/Feb/14/files-to-prompt/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/files-to-prompt/releases/tag/0.5"&gt;files-to-prompt 0.5&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
My &lt;code&gt;files-to-prompt&lt;/code&gt; tool (&lt;a href="https://simonwillison.net/2024/Apr/8/files-to-prompt/"&gt;originally built using Claude 3 Opus back in April&lt;/a&gt;) had been accumulating a bunch of issues and PRs - I finally got around to spending some time with it and pushed a fresh release:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;code&gt;-n/--line-numbers&lt;/code&gt; flag for including line numbers in the output. Thanks, &lt;a href="https://github.com/danclaytondev"&gt;Dan Clayton&lt;/a&gt;. &lt;a href="https://github.com/simonw/files-to-prompt/pull/38"&gt;#38&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Fix for utf-8 handling on Windows. Thanks, &lt;a href="https://github.com/david-jarman"&gt;David Jarman&lt;/a&gt;. &lt;a href="https://github.com/simonw/files-to-prompt/pull/36"&gt;#36&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--ignore&lt;/code&gt; patterns are now matched against directory names as well as file names, unless you pass the new &lt;code&gt;--ignore-files-only&lt;/code&gt; flag. Thanks, &lt;a href="https://github.com/nmpowell"&gt;Nick Powell&lt;/a&gt;. &lt;a href="https://github.com/simonw/files-to-prompt/pull/30"&gt;#30&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I use this tool myself on an almost daily basis - it's fantastic for quickly answering questions about code. Recently I've been plugging it into Gemini 2.0 with its 2 million token context length, running recipes like this one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/bytecodealliance/componentize-py
cd componentize-py
files-to-prompt . -c | llm -m gemini-2.0-pro-exp-02-05 \
  -s 'How does this work? Does it include a python compiler or AST trick of some sort?'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I ran that question against the &lt;a href="https://github.com/bytecodealliance/componentize-py"&gt;bytecodealliance/componentize-py&lt;/a&gt; repo - which provides a tool for turning Python code into compiled WASM - and got &lt;a href="https://gist.github.com/simonw/a9d72e7f903417fb49e1d7a531ee8f97"&gt;this really useful answer&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here's another example. I decided to have o3-mini review how Datasette handles concurrent SQLite connections from async Python code - so I ran this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/simonw/datasette
cd datasette/datasette
files-to-prompt database.py utils/__init__.py -c | \
  llm -m o3-mini -o reasoning_effort high \
  -s 'Output in markdown a detailed analysis of how this code handles the challenge of running SQLite queries from a Python asyncio application. Explain how it works in the first section, then explore the pros and cons of this design. In a final section propose alternative mechanisms that might work better.'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/76c8c433f4a65cf01a5c9121453683ab"&gt;the result&lt;/a&gt;. It did an extremely good job of explaining how my code works - despite being fed just the Python and none of the other documentation. Then it made some solid recommendations for potential alternatives.&lt;/p&gt;
&lt;p&gt;I added a couple of follow-up questions (using &lt;code&gt;llm -c&lt;/code&gt;) which resulted in &lt;a href="https://gist.github.com/simonw/76c8c433f4a65cf01a5c9121453683ab?permalink_comment_id=5438685#gistcomment-5438685"&gt;a full working prototype&lt;/a&gt; of an alternative threadpool mechanism, plus &lt;a href="https://gist.github.com/simonw/76c8c433f4a65cf01a5c9121453683ab?permalink_comment_id=5438691#gistcomment-5438691"&gt;some benchmarks&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One final example: I decided to see if there were any undocumented features in &lt;a href="https://litestream.io/"&gt;Litestream&lt;/a&gt;, so I checked out the repo and ran a prompt against just the &lt;code&gt;.go&lt;/code&gt; files in that project:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/benbjohnson/litestream
cd litestream
files-to-prompt . -e go -c | llm -m o3-mini \
  -s 'Write extensive user documentation for this project in markdown'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once again, o3-mini provided a &lt;a href="https://gist.github.com/simonw/cbf339032f99fee72af5fd5455bc7235"&gt;really impressively detailed&lt;/a&gt; set of unofficial documentation derived purely from reading the source.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/async"&gt;async&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&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/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/litestream"&gt;litestream&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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemini"&gt;gemini&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-reasoning"&gt;llm-reasoning&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="async"/><category term="projects"/><category term="python"/><category term="sqlite"/><category term="ai"/><category term="datasette"/><category term="webassembly"/><category term="litestream"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="gemini"/><category term="llm-reasoning"/><category term="files-to-prompt"/></entry><entry><title>Gemini 2.0 is now available to everyone</title><link href="https://simonwillison.net/2025/Feb/5/gemini-2/#atom-tag" rel="alternate"/><published>2025-02-05T16:37:29+00:00</published><updated>2025-02-05T16:37:29+00:00</updated><id>https://simonwillison.net/2025/Feb/5/gemini-2/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.google/technology/google-deepmind/gemini-model-updates-february-2025/"&gt;Gemini 2.0 is now available to everyone&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Big new Gemini 2.0 releases today:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gemini 2.0 Pro (Experimental)&lt;/strong&gt; is Google's "best model yet for coding performance and complex prompts" - currently available as a free preview.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gemini 2.0 Flash&lt;/strong&gt; is now generally available.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Gemini 2.0 Flash-Lite&lt;/strong&gt; looks particularly interesting:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We’ve gotten a lot of positive feedback on the price and speed of 1.5 Flash. We wanted to keep improving quality, while still maintaining cost and speed. So today, we’re introducing 2.0 Flash-Lite, a new model that has better quality than 1.5 Flash, at the same speed and cost. It outperforms 1.5 Flash on the majority of benchmarks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means Gemini 2.0 Flash-Lite is priced at 7.5c/million input tokens and 30c/million output tokens - half the price of OpenAI's GPT-4o mini (15c/60c).&lt;/p&gt;
&lt;p&gt;Gemini 2.0 Flash isn't &lt;a href="https://ai.google.dev/pricing#2_0flash"&gt;much more expensive&lt;/a&gt;:  10c/million for text/image input, 70c/million for audio input, 40c/million for output. Again, cheaper than GPT-4o mini.&lt;/p&gt;
&lt;p&gt;I pushed a new &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; plugin release, &lt;a href="https://github.com/simonw/llm-gemini/releases/tag/0.10"&gt;llm-gemini 0.10&lt;/a&gt;, adding support for the three new models:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm install -U llm-gemini
llm keys set gemini
# paste API key here
llm -m gemini-2.0-flash "impress me"
llm -m gemini-2.0-flash-lite-preview-02-05 "impress me"
llm -m gemini-2.0-pro-exp-02-05 "impress me"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/411d032a84f02e8300b1f48df54913b7"&gt;the output&lt;/a&gt; for those three prompts.&lt;/p&gt;
&lt;p&gt;I ran &lt;code&gt;Generate an SVG of a pelican riding a bicycle&lt;/code&gt; through the three new models. Here are the results, cheapest to most expensive:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;gemini-2.0-flash-lite-preview-02-05&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="This is not great. The bicycle is a trapezoid. The pelican is very warped and has a orange diamond beak above its head." src="https://static.simonwillison.net/static/2025/gemini-2.0-flash-lite-preview-02-05.svg" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;gemini-2.0-flash&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="The bicycle is better but the pelican is yellow and looks more like a baby chick. Its beak is squashed against the side of the image." src="https://static.simonwillison.net/static/2025/gemini-2.0-flash.svg" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;gemini-2.0-pro-exp-02-05&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="This one is pleasingly avant-garde. The bicycle does at least have two wheels joined by a frame. The pelican is a fun shape, and it has a beak with a curved orange top and a curved yellow bottom." src="https://static.simonwillison.net/static/2025/gemini-2.0-pro-exp-02-05.svg" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/simonw/e50eea6d4f746fb3bc5543d106ffa8e7"&gt;Full transcripts here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I also ran the same prompt I &lt;a href="https://simonwillison.net/2025/Feb/5/o3-mini-documentation/"&gt;tried with o3-mini the other day&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /tmp
git clone https://github.com/simonw/datasette
cd datasette
files-to-prompt datasette -e py -c | \
  llm -m gemini-2.0-pro-exp-02-05 \
  -s 'write extensive documentation for how the permissions system works, as markdown' \
  -o max_output_tokens 10000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/e20697b792dc62843f8ab5c25bfab15b"&gt;the result from that&lt;/a&gt; - you can compare that to &lt;a href="https://gist.github.com/simonw/4a13c4b10176d7b8e3d1260f5dcc9de3"&gt;o3-mini's result here&lt;/a&gt;.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/google"&gt;google&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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gemini"&gt;gemini&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-pricing"&gt;llm-pricing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pelican-riding-a-bicycle"&gt;pelican-riding-a-bicycle&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-release"&gt;llm-release&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="google"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="llm"/><category term="gemini"/><category term="llm-pricing"/><category term="pelican-riding-a-bicycle"/><category term="llm-release"/><category term="files-to-prompt"/></entry><entry><title>o3-mini is really good at writing internal documentation</title><link href="https://simonwillison.net/2025/Feb/5/o3-mini-documentation/#atom-tag" rel="alternate"/><published>2025-02-05T06:07:40+00:00</published><updated>2025-02-05T06:07:40+00:00</updated><id>https://simonwillison.net/2025/Feb/5/o3-mini-documentation/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://gist.github.com/simonw/4a13c4b10176d7b8e3d1260f5dcc9de3"&gt;o3-mini is really good at writing internal documentation&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I wanted to refresh my knowledge of how the Datasette permissions system works today. I already have &lt;a href="https://docs.datasette.io/en/latest/authentication.html"&gt;extensive hand-written documentation&lt;/a&gt; for that, but I thought it would be interesting to see if I could derive any insights from running an LLM against the codebase.&lt;/p&gt;
&lt;p&gt;o3-mini has an input limit of 200,000 tokens. I used &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; and my &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; tool to generate the documentation like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;&lt;span class="pl-c1"&gt;cd&lt;/span&gt; /tmp
git clone https://github.com/simonw/datasette
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; datasette
files-to-prompt datasette -e py -c &lt;span class="pl-k"&gt;|&lt;/span&gt; \
  llm -m o3-mini -s \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;write extensive documentation for how the permissions system works, as markdown&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;files-to-prompt&lt;/code&gt; command is fed the &lt;a href="https://github.com/simonw/datasette/tree/main/datasette"&gt;datasette&lt;/a&gt; subdirectory, which contains just the source code for the application - omitting tests (in &lt;code&gt;tests/&lt;/code&gt;) and documentation (in &lt;code&gt;docs/&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;-e py&lt;/code&gt; option causes it to only include files with a &lt;code&gt;.py&lt;/code&gt; extension - skipping all of the HTML and JavaScript files in that hierarchy.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;-c&lt;/code&gt; option causes it to output Claude's XML-ish format - a format that works great with other LLMs too.&lt;/p&gt;
&lt;p&gt;You can see the output of that command &lt;a href="https://gist.github.com/simonw/1922544763b08c76f0b904e2ece364ea"&gt;in this Gist&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then I pipe that result into LLM, requesting the &lt;code&gt;o3-mini&lt;/code&gt; OpenAI model and passing the following system prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;write extensive documentation for how the permissions system works, as markdown&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Specifically requesting Markdown &lt;a href="https://simonwillison.net/2025/Feb/2/openai-reasoning-models-advice-on-prompting/"&gt;is important&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The prompt used 99,348 input tokens and produced 3,118 output tokens (320 of those were invisible reasoning tokens). That's &lt;a href="https://tools.simonwillison.net/llm-prices"&gt;a cost&lt;/a&gt; of 12.3 cents.&lt;/p&gt;
&lt;p&gt;Honestly, &lt;a href="https://gist.github.com/simonw/4a13c4b10176d7b8e3d1260f5dcc9de3"&gt;the results&lt;/a&gt; are fantastic. I had to double-check that I hadn't accidentally fed in the documentation by mistake.&lt;/p&gt;
&lt;p&gt;(It's possible that the model is picking up additional information about Datasette in its training set, but I've seen similar &lt;a href="https://gist.github.com/simonw/adf64108d65cd5c10ac9fce953ab437e"&gt;high quality results&lt;/a&gt; from other, newer libraries so I don't think that's a significant factor.)&lt;/p&gt;
&lt;p&gt;In this case I already had extensive written documentation of my own, but this was still a useful refresher to help confirm that the code matched my mental model of how everything works.&lt;/p&gt;
&lt;p&gt;Documentation of project internals as a category is notorious for going out of date. Having tricks like this to derive usable how-it-works documentation from existing codebases in just a few seconds and at a cost of a few cents is wildly valuable.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/documentation"&gt;documentation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-reasoning"&gt;llm-reasoning&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/o3"&gt;o3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="documentation"/><category term="ai"/><category term="datasette"/><category term="openai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="llm-reasoning"/><category term="o3"/><category term="files-to-prompt"/></entry><entry><title>OpenAI reasoning models: Advice on prompting</title><link href="https://simonwillison.net/2025/Feb/2/openai-reasoning-models-advice-on-prompting/#atom-tag" rel="alternate"/><published>2025-02-02T20:56:27+00:00</published><updated>2025-02-02T20:56:27+00:00</updated><id>https://simonwillison.net/2025/Feb/2/openai-reasoning-models-advice-on-prompting/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://platform.openai.com/docs/guides/reasoning#advice-on-prompting"&gt;OpenAI reasoning models: Advice on prompting&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
OpenAI's documentation for their o1 and o3 "reasoning models" includes some interesting tips on how to best prompt them:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Developer messages are the new system messages:&lt;/strong&gt; Starting with &lt;code&gt;o1-2024-12-17&lt;/code&gt;, reasoning models support &lt;code&gt;developer&lt;/code&gt; messages rather than &lt;code&gt;system&lt;/code&gt; messages, to align with the &lt;a href="https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command"&gt;chain of command behavior described in the model spec&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This appears to be a purely aesthetic change made for consistency with their &lt;a href="https://simonwillison.net/2024/Apr/23/the-instruction-hierarchy/"&gt;instruction hierarchy&lt;/a&gt; concept. As far as I can tell the old &lt;code&gt;system&lt;/code&gt; prompts continue to work exactly as before - you're encouraged to use the new &lt;code&gt;developer&lt;/code&gt; message type but it has no impact on what actually happens.&lt;/p&gt;
&lt;p&gt;Since my &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; tool already bakes in a &lt;code&gt;llm --system "system prompt"&lt;/code&gt; option which works across multiple different models from different providers I'm not going to rush to adopt this new language!&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use delimiters for clarity:&lt;/strong&gt; Use delimiters like markdown, XML tags, and section titles to clearly indicate distinct parts of the input, helping the model interpret different sections appropriately.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Anthropic have been encouraging &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/use-xml-tags"&gt;XML-ish delimiters&lt;/a&gt; for a while (I say -ish because there's no requirement that the resulting prompt is valid XML). My &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; tool has a &lt;code&gt;-c&lt;/code&gt; option which outputs Claude-style XML, and in my experiments this same option works great with o1 and o3 too:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;git clone https://github.com/tursodatabase/limbo
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; limbo/bindings/python

files-to-prompt &lt;span class="pl-c1"&gt;.&lt;/span&gt; -c &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m o3-mini \
  -o reasoning_effort high \
  --system &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Write a detailed README with extensive usage examples&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Limit additional context in retrieval-augmented generation (RAG):&lt;/strong&gt; When providing additional context or documents, include only the most relevant information to prevent the model from overcomplicating its response.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This makes me thing that o1/o3 are not good models to implement RAG on at all - with RAG I like to be able to dump as much extra context into the prompt as possible and leave it to the models to figure out what's relevant.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Try zero shot first, then few shot if needed:&lt;/strong&gt; Reasoning models often don't need few-shot examples to produce good results, so try to write prompts without examples first. If you have more complex requirements for your desired output, it may help to include a few examples of inputs and desired outputs in your prompt. Just ensure that the examples align very closely with your prompt instructions, as discrepancies between the two may produce poor results.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Providing examples remains the single most powerful prompting tip I know, so it's interesting to see advice here to only switch to examples if zero-shot doesn't work out.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Be very specific about your end goal:&lt;/strong&gt; In your instructions, try to give very specific parameters for a successful response, and encourage the model to keep reasoning and iterating until it matches your success criteria.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This makes sense: reasoning models "think" until they reach a conclusion, so making the goal as unambiguous as possible leads to better results.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Markdown formatting:&lt;/strong&gt; Starting with &lt;code&gt;o1-2024-12-17&lt;/code&gt;, reasoning models in the API will avoid generating responses with markdown formatting. To signal to the model when you &lt;strong&gt;do&lt;/strong&gt; want markdown formatting in the response, include the string &lt;code&gt;Formatting re-enabled&lt;/code&gt; on the first line of your &lt;code&gt;developer&lt;/code&gt; message.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This one was a &lt;em&gt;real shock&lt;/em&gt; to me! I noticed that o3-mini was outputting &lt;code&gt;•&lt;/code&gt; characters instead of Markdown &lt;code&gt;*&lt;/code&gt; bullets and initially thought &lt;a href="https://twitter.com/simonw/status/1886121477822648441"&gt;that was a bug&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I first saw this while running this prompt against &lt;a href="https://github.com/tursodatabase/limbo/tree/main/bindings/python"&gt;limbo/bindings/python&lt;/a&gt; using &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;git clone https://github.com/tursodatabase/limbo
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; limbo/bindings/python

files-to-prompt &lt;span class="pl-c1"&gt;.&lt;/span&gt; -c &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m o3-mini \
  -o reasoning_effort high \
  --system &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Write a detailed README with extensive usage examples&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here's the &lt;a href="https://gist.github.com/simonw/f8283d68e9bd7ad3f140d52cad6874a7"&gt;full result&lt;/a&gt;, which includes text like this (note the weird bullets):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Features
--------
• High‑performance, in‑process database engine written in Rust  
• SQLite‑compatible SQL interface  
• Standard Python DB‑API 2.0–style connection and cursor objects
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I ran it again with this modified prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Formatting re-enabled. Write a detailed README with extensive usage examples.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And this time got back &lt;a href="https://gist.github.com/simonw/adf64108d65cd5c10ac9fce953ab437e"&gt;proper Markdown, rendered in this Gist&lt;/a&gt;. That did a really good job, and included bulleted lists using this valid Markdown syntax instead:&lt;/p&gt;
&lt;div class="highlight highlight-text-md"&gt;&lt;pre&gt;&lt;span class="pl-v"&gt;-&lt;/span&gt; &lt;span class="pl-s"&gt;**&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt;&lt;span class="pl-c1"&gt;make test&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt;&lt;span class="pl-s"&gt;**&lt;/span&gt;: Run tests using pytest.
&lt;span class="pl-v"&gt;-&lt;/span&gt; &lt;span class="pl-s"&gt;**&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt;&lt;span class="pl-c1"&gt;make lint&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt;&lt;span class="pl-s"&gt;**&lt;/span&gt;: Run linters (via &lt;span class="pl-s"&gt;[&lt;/span&gt;ruff&lt;span class="pl-s"&gt;]&lt;/span&gt;&lt;span class="pl-s"&gt;(&lt;/span&gt;&lt;span class="pl-corl"&gt;https://github.com/astral-sh/ruff&lt;/span&gt;&lt;span class="pl-s"&gt;)&lt;/span&gt;).
&lt;span class="pl-v"&gt;-&lt;/span&gt; &lt;span class="pl-s"&gt;**&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt;&lt;span class="pl-c1"&gt;make check-requirements&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt;&lt;span class="pl-s"&gt;**&lt;/span&gt;: Validate that the &lt;span class="pl-s"&gt;`&lt;/span&gt;&lt;span class="pl-c1"&gt;requirements.txt&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt; files are in sync with &lt;span class="pl-s"&gt;`&lt;/span&gt;&lt;span class="pl-c1"&gt;pyproject.toml&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt;.
&lt;span class="pl-v"&gt;-&lt;/span&gt; &lt;span class="pl-s"&gt;**&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt;&lt;span class="pl-c1"&gt;make compile-requirements&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt;&lt;span class="pl-s"&gt;**&lt;/span&gt;: Compile the &lt;span class="pl-s"&gt;`&lt;/span&gt;&lt;span class="pl-c1"&gt;requirements.txt&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;/span&gt; files using pip-tools.&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="Py-Limbo. Py-Limbo is a lightweight, in-process, OLTP (Online Transaction Processing) database management system built as a Python extension module on top of Rust. It is designed to be compatible with SQLite in both usage and API, while offering an opportunity to experiment with Rust-backed database functionality. Note: Py-Limbo is a work-in-progress (Alpha stage) project. Some features (e.g. transactions, executemany, fetchmany) are not yet supported. Table of Contents - then a hierarchical nested table of contents." src="https://static.simonwillison.net/static/2025/pylimbo-docs.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;(Using LLMs like this to get me off the ground with under-documented libraries is a trick I use several times a month.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: &lt;a href="https://twitter.com/nikunjhanda/status/1886169547197264226"&gt;OpenAI's Nikunj Handa&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;we agree this is weird! fwiw, it’s a temporary thing we had to do for the existing o-series models. we’ll fix this in future releases so that you can go back to naturally prompting for markdown or no-markdown.&lt;/p&gt;
&lt;/blockquote&gt;

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/documentation"&gt;documentation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/markdown"&gt;markdown&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/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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rag"&gt;rag&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/o1"&gt;o1&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-reasoning"&gt;llm-reasoning&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/o3"&gt;o3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/limbo"&gt;limbo&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/system-prompts"&gt;system-prompts&lt;/a&gt;&lt;/p&gt;



</summary><category term="documentation"/><category term="markdown"/><category term="ai"/><category term="openai"/><category term="prompt-engineering"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="rag"/><category term="o1"/><category term="llm-reasoning"/><category term="o3"/><category term="limbo"/><category term="files-to-prompt"/><category term="system-prompts"/></entry><entry><title>Qwen2.5-1M: Deploy Your Own Qwen with Context Length up to 1M Tokens</title><link href="https://simonwillison.net/2025/Jan/26/qwen25-1m/#atom-tag" rel="alternate"/><published>2025-01-26T18:54:45+00:00</published><updated>2025-01-26T18:54:45+00:00</updated><id>https://simonwillison.net/2025/Jan/26/qwen25-1m/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://qwenlm.github.io/blog/qwen2.5-1m/"&gt;Qwen2.5-1M: Deploy Your Own Qwen with Context Length up to 1M Tokens&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Very significant new release from Alibaba's Qwen team. Their openly licensed (sometimes Apache 2, sometimes Qwen license, I've had trouble keeping up) Qwen 2.5 LLM previously had an input token limit of 128,000 tokens. This new model increases that to 1 million, using a new technique called &lt;strong&gt;Dual Chunk Attention&lt;/strong&gt;, first described in &lt;a href="https://arxiv.org/abs/2402.17463"&gt;this paper&lt;/a&gt; from February 2024.&lt;/p&gt;
&lt;p&gt;They've released two models on Hugging Face: &lt;a href="https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-1M"&gt;Qwen2.5-7B-Instruct-1M&lt;/a&gt; and &lt;a href="https://huggingface.co/Qwen/Qwen2.5-14B-Instruct-1M"&gt;Qwen2.5-14B-Instruct-1M&lt;/a&gt;, both requiring CUDA and both under an Apache 2.0 license.&lt;/p&gt;
&lt;p&gt;You'll need a &lt;em&gt;lot&lt;/em&gt; of VRAM to run them at their full capacity:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;VRAM Requirement for processing 1 million-token sequences:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Qwen2.5-7B-Instruct-1M&lt;/strong&gt;: At least 120GB VRAM (total across GPUs).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Qwen2.5-14B-Instruct-1M&lt;/strong&gt;: At least 320GB VRAM (total across GPUs).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If your GPUs do not have sufficient VRAM, you can still use Qwen2.5-1M models for shorter tasks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Qwen recommend using their custom fork of vLLM to serve the models:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can also use the previous framework that supports Qwen2.5 for inference, but accuracy degradation may occur for sequences exceeding 262,144 tokens.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;GGUF quantized versions of the models are already starting to show up. LM Studio's "official model curator" &lt;a href="https://huggingface.co/bartowski"&gt;Bartowski&lt;/a&gt; published &lt;a href="https://huggingface.co/lmstudio-community/Qwen2.5-7B-Instruct-1M-GGUF"&gt;lmstudio-community/Qwen2.5-7B-Instruct-1M-GGUF&lt;/a&gt; and &lt;a href="https://huggingface.co/lmstudio-community/Qwen2.5-14B-Instruct-1M-GGUF"&gt;lmstudio-community/Qwen2.5-14B-Instruct-1M-GGUF&lt;/a&gt; - sizes range from 4.09GB to 8.1GB for the 7B model and 7.92GB to 15.7GB for the 14B.&lt;/p&gt;
&lt;p&gt;These might not work well yet with the full context lengths as the underlying &lt;code&gt;llama.cpp&lt;/code&gt; library may need some changes.&lt;/p&gt;
&lt;p&gt;I tried running the 8.1GB 7B model using &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt; on my Mac like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ollama run hf.co/lmstudio-community/Qwen2.5-7B-Instruct-1M-GGUF:Q8_0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then with &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm install llm-ollama
llm models -q qwen # To search for the model ID
# I set a shorter q1m alias:
llm aliases set q1m hf.co/lmstudio-community/Qwen2.5-7B-Instruct-1M-GGUF:Q8_0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I tried piping a large prompt in using &lt;a href="https://pypi.org/project/files-to-prompt/"&gt;files-to-prompt&lt;/a&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;files-to-prompt ~/Dropbox/Development/llm -e py -c | llm -m q1m 'describe this codebase in detail'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That should give me every Python file in my &lt;a href="https://github.com/simonw/llm"&gt;llm project&lt;/a&gt;. Piping that through &lt;a href="https://pypi.org/project/ttok/"&gt;ttok&lt;/a&gt; first told me this was 63,014 OpenAI tokens, I expect that count is similar for Qwen.&lt;/p&gt;
&lt;p&gt;The result &lt;a href="https://gist.github.com/simonw/ace6ff544dddabb0797b8d20d84627a8#response"&gt;was disappointing&lt;/a&gt;: it appeared to describe just the last Python file that stream. Then I noticed the token usage report:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2,048 input, 999 output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This suggests to me that something's not working right here - maybe the Ollama hosting framework is truncating the input, or maybe there's a problem with the GGUF I'm using?&lt;/p&gt;
&lt;p&gt;I'll update this post when I figure out how to run longer prompts through the new Qwen model using GGUF weights on a Mac.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; It &lt;a href="https://news.ycombinator.com/item?id=42832838#42833427"&gt;turns out&lt;/a&gt; Ollama has a &lt;code&gt;num_ctx&lt;/code&gt; option which defaults to 2048, affecting the input context length. I tried this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;files-to-prompt \
  ~/Dropbox/Development/llm \
  -e py -c | \
llm -m q1m 'describe this codebase in detail' \
 -o num_ctx 80000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But I quickly ran out of RAM (I have 64GB but a lot of that was in use already) and hit &lt;code&gt;Ctrl+C&lt;/code&gt; to avoid crashing my computer. I need to experiment a bit to figure out how much RAM is used for what context size.&lt;/p&gt;
&lt;p&gt;Awni Hannun &lt;a href="https://twitter.com/awnihannun/status/1883611098081099914"&gt;shared tips&lt;/a&gt; for running &lt;a href="https://huggingface.co/mlx-community/Qwen2.5-7B-Instruct-1M-4bit"&gt;mlx-community/Qwen2.5-7B-Instruct-1M-4bit&lt;/a&gt; using MLX,  which should work for up to 250,000 tokens. They ran 120,000 tokens and reported:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Peak RAM for prompt filling was 22GB&lt;/li&gt;
&lt;li&gt;Peak RAM for generation 12GB&lt;/li&gt;
&lt;li&gt;Prompt filling took 350 seconds on an M2 Ultra&lt;/li&gt;
&lt;li&gt;Generation ran at 31 tokens-per-second on M2 Ultra&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/reach_vb/status/1883560095176708163"&gt;VB&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/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/local-llms"&gt;local-llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/qwen"&gt;qwen&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mlx"&gt;mlx&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ollama"&gt;ollama&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/long-context"&gt;long-context&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llama-cpp"&gt;llama-cpp&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-release"&gt;llm-release&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/lm-studio"&gt;lm-studio&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-in-china"&gt;ai-in-china&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai"/><category term="generative-ai"/><category term="local-llms"/><category term="llms"/><category term="llm"/><category term="qwen"/><category term="mlx"/><category term="ollama"/><category term="long-context"/><category term="llama-cpp"/><category term="llm-release"/><category term="files-to-prompt"/><category term="lm-studio"/><category term="ai-in-china"/></entry><entry><title>Introducing Limbo: A complete rewrite of SQLite in Rust</title><link href="https://simonwillison.net/2024/Dec/10/introducing-limbo/#atom-tag" rel="alternate"/><published>2024-12-10T19:25:21+00:00</published><updated>2024-12-10T19:25:21+00:00</updated><id>https://simonwillison.net/2024/Dec/10/introducing-limbo/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://turso.tech/blog/introducing-limbo-a-complete-rewrite-of-sqlite-in-rust"&gt;Introducing Limbo: A complete rewrite of SQLite in Rust&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This looks absurdly ambitious:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Our goal is to build a reimplementation of SQLite from scratch, fully compatible at the language and file format level,  with the same or higher reliability SQLite is known for, but with full memory safety and on a new, modern architecture.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Turso team behind it have been maintaining their &lt;a href="https://github.com/tursodatabase/libsql"&gt;libSQL&lt;/a&gt; fork for two years now, so they're well equipped to take on a challenge of this magnitude.&lt;/p&gt;
&lt;p&gt;SQLite is justifiably famous for its &lt;a href="https://www.sqlite.org/testing.html"&gt;meticulous approach to testing&lt;/a&gt;. Limbo plans to take an entirely different approach based on "Deterministic Simulation Testing" - a modern technique &lt;a href="https://antithesis.com/blog/is_something_bugging_you/"&gt;pioneered by FoundationDB&lt;/a&gt; and now spearheaded by &lt;a href="https://antithesis.com/"&gt;Antithesis&lt;/a&gt;, the company Turso have been working with on their previous testing projects.&lt;/p&gt;
&lt;p&gt;Another bold claim (emphasis mine):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We have both added DST facilities to the core of the database, and partnered with Antithesis to achieve a level of reliability in the database that lives up to SQLite’s reputation.&lt;/p&gt;
&lt;p&gt;[...] With DST, &lt;strong&gt;we believe we can achieve an even higher degree of robustness than SQLite&lt;/strong&gt;, since it is easier to simulate unlikely scenarios in a simulator, test years of execution with different event orderings, and upon finding issues, reproduce them 100% reliably.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The two most interesting features that Limbo is planning to offer are first-party WASM support and fully asynchronous I/O:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SQLite itself has a synchronous interface, meaning driver authors who want asynchronous behavior need to have the extra complication of using helper threads. Because SQLite queries tend to be fast, since no network round trips are involved, a lot of those drivers just settle for a synchronous interface. [...]&lt;/p&gt;
&lt;p&gt;Limbo is designed to be asynchronous from the ground up. It extends &lt;code&gt;sqlite3_step&lt;/code&gt;, the main entry point API to SQLite, to be asynchronous, allowing it to return to the caller if data is not ready to consume immediately.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; provides an &lt;a href="https://docs.datasette.io/en/stable/internals.html#await-db-execute-sql"&gt;async API&lt;/a&gt; for executing SQLite queries which is backed by all manner of complex thread management - I would be very interested in a native asyncio Python library for talking to SQLite database files.&lt;/p&gt;
&lt;p&gt;I successfully tried out Limbo's &lt;a href="https://github.com/tursodatabase/limbo/tree/main/bindings/python"&gt;Python bindings&lt;/a&gt; against a demo SQLite test database using &lt;code&gt;uv&lt;/code&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv run --with pylimbo python
&amp;gt;&amp;gt;&amp;gt; import limbo
&amp;gt;&amp;gt;&amp;gt; conn = limbo.connect("/tmp/demo.db")
&amp;gt;&amp;gt;&amp;gt; cursor = conn.cursor()
&amp;gt;&amp;gt;&amp;gt; print(cursor.execute("select * from foo").fetchall())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It crashed when I tried against a more complex SQLite database that included SQLite FTS tables.&lt;/p&gt;
&lt;p&gt;The Python bindings aren't yet documented, so I piped them through &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; and had the new &lt;code&gt;google-exp-1206&lt;/code&gt; model write &lt;a href="https://gist.github.com/simonw/bd1822f372c406d17ed24772f8b93eea"&gt;this initial documentation&lt;/a&gt; for me:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;files-to-prompt limbo/bindings/python -c | llm -m gemini-exp-1206 -s 'write extensive usage documentation in markdown, including realistic usage examples'
&lt;/code&gt;&lt;/pre&gt;

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/documentation"&gt;documentation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/uv"&gt;uv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/limbo"&gt;limbo&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="documentation"/><category term="open-source"/><category term="python"/><category term="sqlite"/><category term="rust"/><category term="ai-assisted-programming"/><category term="llm"/><category term="uv"/><category term="limbo"/><category term="files-to-prompt"/></entry><entry><title>Generating documentation from tests using files-to-prompt and LLM</title><link href="https://simonwillison.net/2024/Nov/5/docs-from-tests/#atom-tag" rel="alternate"/><published>2024-11-05T22:37:20+00:00</published><updated>2024-11-05T22:37:20+00:00</updated><id>https://simonwillison.net/2024/Nov/5/docs-from-tests/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/llms/docs-from-tests"&gt;Generating documentation from tests using files-to-prompt and LLM&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I was experimenting with the &lt;a href="https://github.com/bytecodealliance/wasmtime-py"&gt;wasmtime-py&lt;/a&gt; Python library today (for executing WebAssembly programs from inside CPython) and I found the existing &lt;a href="https://bytecodealliance.github.io/wasmtime-py/"&gt;API docs&lt;/a&gt; didn't quite show me what I wanted to know.&lt;/p&gt;
&lt;p&gt;The project has a &lt;a href="https://github.com/bytecodealliance/wasmtime-py/tree/main/tests"&gt;comprehensive test suite&lt;/a&gt; so I tried seeing if I could generate documentation using that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /tmp
git clone https://github.com/bytecodealliance/wasmtime-py
files-to-prompt -e py wasmtime-py/tests -c | \
  llm -m claude-3.5-sonnet -s \
  'write detailed usage documentation including realistic examples'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;More &lt;a href="https://til.simonwillison.net/llms/docs-from-tests"&gt;notes in my TIL&lt;/a&gt;. You can see the &lt;a href="https://gist.github.com/simonw/351cffbd254af5cbf329377fb95fcc13"&gt;full Claude transcript here&lt;/a&gt; - I think this worked really well!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/documentation"&gt;documentation&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&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/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="documentation"/><category term="ai"/><category term="webassembly"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="claude"/><category term="claude-3-5-sonnet"/><category term="files-to-prompt"/></entry><entry><title>files-to-prompt 0.4</title><link href="https://simonwillison.net/2024/Oct/16/files-to-prompt-04/#atom-tag" rel="alternate"/><published>2024-10-16T23:29:08+00:00</published><updated>2024-10-16T23:29:08+00:00</updated><id>https://simonwillison.net/2024/Oct/16/files-to-prompt-04/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/files-to-prompt/releases/tag/0.4"&gt;files-to-prompt 0.4&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New release of my &lt;a href="https://simonwillison.net/2024/Apr/8/files-to-prompt/"&gt;files-to-prompt tool&lt;/a&gt; adding an option for filtering just for files with a specific extension.&lt;/p&gt;
&lt;p&gt;The following command will output Claude XML-style markup for all Python and Markdown files in the current directory, and copy that to the macOS clipboard ready to be pasted into an LLM:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;files-to-prompt . -e py -e md -c | pbcopy
&lt;/code&gt;&lt;/pre&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="projects"/><category term="python"/><category term="llms"/><category term="files-to-prompt"/></entry><entry><title>An LLM TDD loop</title><link href="https://simonwillison.net/2024/Oct/13/an-llm-tdd-loop/#atom-tag" rel="alternate"/><published>2024-10-13T19:37:47+00:00</published><updated>2024-10-13T19:37:47+00:00</updated><id>https://simonwillison.net/2024/Oct/13/an-llm-tdd-loop/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://codeinthehole.com/tips/llm-tdd-loop-script/"&gt;An LLM TDD loop&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Super neat demo by David Winterbottom, who wrapped my &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; and &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; tools in &lt;a href="https://gist.github.com/codeinthehole/d12af317a76b43423b111fd6d508c4fc"&gt;a short Bash script&lt;/a&gt; that can be fed a file full of Python unit tests and an empty implementation file and will then iterate on that file in a loop until the tests pass.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="testing"/><category term="ai"/><category term="pytest"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="files-to-prompt"/></entry><entry><title>lm.rs: run inference on Language Models locally on the CPU with Rust</title><link href="https://simonwillison.net/2024/Oct/11/lmrs/#atom-tag" rel="alternate"/><published>2024-10-11T19:33:34+00:00</published><updated>2024-10-11T19:33:34+00:00</updated><id>https://simonwillison.net/2024/Oct/11/lmrs/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/samuel-vitorino/lm.rs"&gt;lm.rs: run inference on Language Models locally on the CPU with Rust&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Impressive new LLM inference implementation in Rust by Samuel Vitorino. I tried it just now on an M2 Mac with 64GB of RAM and got very snappy performance for &lt;a href="https://huggingface.co/samuel-vitorino/Llama-3.2-1B-Instruct-Q8_0-LMRS"&gt;this Q8 Llama 3.2 1B&lt;/a&gt;, with Activity Monitor reporting 980% CPU usage over 13 threads.&lt;/p&gt;
&lt;p&gt;Here's how I compiled the library and ran the model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /tmp
git clone https://github.com/samuel-vitorino/lm.rs
cd lm.rs
RUSTFLAGS="-C target-cpu=native" cargo build --release --bin chat
curl -LO 'https://huggingface.co/samuel-vitorino/Llama-3.2-1B-Instruct-Q8_0-LMRS/resolve/main/tokenizer.bin?download=true'
curl -LO 'https://huggingface.co/samuel-vitorino/Llama-3.2-1B-Instruct-Q8_0-LMRS/resolve/main/llama3.2-1b-it-q80.lmrs?download=true'
./target/release/chat --model llama3.2-1b-it-q80.lmrs --show-metrics
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;--show-metrics&lt;/code&gt; option added this at the end of a response:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Speed: 26.41 tok/s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It looks like the performance is helped by two key dependencies: &lt;a href="https://crates.io/crates/wide"&gt;wide&lt;/a&gt;, which provides data types optimized for SIMD operations and &lt;a href="https://crates.io/crates/rayon"&gt;rayon&lt;/a&gt; for running parallel iterators across multiple cores (used &lt;a href="https://github.com/samuel-vitorino/lm.rs/blob/4a27af0ea07e284cf2a9c7cd1c984e484f143804/src/functional.rs#L136-L153"&gt;for matrix multiplication&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;(I used LLM and &lt;code&gt;files-to-prompt&lt;/code&gt; to &lt;a href="https://gist.github.com/simonw/19ce7d66bcd9a9efc46e25354a2f5b3c"&gt;help figure this out&lt;/a&gt;.)

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=41811078"&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/rust"&gt;rust&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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai"/><category term="rust"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="files-to-prompt"/></entry><entry><title>Llama 3.2</title><link href="https://simonwillison.net/2024/Sep/25/llama-32/#atom-tag" rel="alternate"/><published>2024-09-25T20:28:04+00:00</published><updated>2024-09-25T20:28:04+00:00</updated><id>https://simonwillison.net/2024/Sep/25/llama-32/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://ai.meta.com/blog/llama-3-2-connect-2024-vision-edge-mobile-devices/"&gt;Llama 3.2&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
In further evidence that AI labs are terrible at naming things, Llama 3.2 is a &lt;em&gt;huge&lt;/em&gt; upgrade to the Llama 3 series - they've released their first multi-modal vision models!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Today, we’re releasing Llama 3.2, which includes small and medium-sized vision LLMs (11B and 90B), and lightweight, text-only models (1B and 3B) that fit onto edge and mobile devices, including pre-trained and instruction-tuned versions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The 1B and 3B text-only models are exciting too, with a 128,000 token context length and optimized for edge devices (Qualcomm and MediaTek hardware get called out specifically).&lt;/p&gt;
&lt;p&gt;Meta partnered directly with Ollama to help with distribution, &lt;a href="https://ollama.com/blog/llama3.2"&gt;here's the Ollama blog post&lt;/a&gt;. They only support the two smaller text-only models at the moment - this command will get the 3B model (2GB):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ollama run llama3.2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And for the 1B model (a 1.3GB download):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ollama run llama3.2:1b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I had to first upgrade my Ollama by clicking on the icon in my macOS task tray and selecting "Restart to update". &lt;/p&gt;
&lt;p&gt;The two vision models are coming to Ollama "very soon".&lt;/p&gt;
&lt;p&gt;Once you have fetched the Ollama model you can access it from my &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; command-line tool like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipx install llm
llm install llm-ollama
llm chat -m llama3.2:1b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I tried running my &lt;a href="https://github.com/simonw/djp"&gt;djp codebase&lt;/a&gt; through that tiny 1B model just now and got &lt;a href="https://gist.github.com/simonw/64c5f5b111fe473999144932bef4218b"&gt;a surprisingly good result&lt;/a&gt; - by no means comprehensive, but way better than I would ever expect from a model of that size:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;files-to-prompt **/*.py -c | llm -m llama3.2:1b --system 'describe this code'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's a portion of the output:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The first section defines several test functions using the &lt;code&gt;@djp.hookimpl&lt;/code&gt; decorator from the djp library. These hook implementations allow you to intercept and manipulate Django's behavior.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;test_middleware_order&lt;/code&gt;: This function checks that the middleware order is correct by comparing the &lt;code&gt;MIDDLEWARE&lt;/code&gt; setting with a predefined list.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;test_middleware&lt;/code&gt;: This function tests various aspects of middleware:&lt;/li&gt;
&lt;li&gt;It retrieves the response from the URL &lt;code&gt;/from-plugin/&lt;/code&gt; using the &lt;code&gt;Client&lt;/code&gt; object, which simulates a request to this view.&lt;/li&gt;
&lt;li&gt;It checks that certain values are present in the response:&lt;ul&gt;
&lt;li&gt;&lt;code&gt;X-DJP-Middleware-After&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-DJP-Middleware&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-DJP-Middleware-Before&lt;/code&gt; [...]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I found the GGUF file that had been downloaded by Ollama in my &lt;code&gt;~/.ollama/models/blobs&lt;/code&gt; directory. The following command let me run that model directly in LLM using the &lt;a href="https://github.com/simonw/llm-gguf"&gt;llm-gguf&lt;/a&gt; plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm install llm-gguf
llm gguf register-model ~/.ollama/models/blobs/sha256-74701a8c35f6c8d9a4b91f3f3497643001d63e0c7a84e085bed452548fa88d45 -a llama321b
llm chat -m llama321b
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Meta themselves claim impressive performance against other existing models:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Our evaluation suggests that the Llama 3.2 vision models are competitive with leading foundation models, Claude 3 Haiku and GPT4o-mini on image recognition and a range of visual understanding tasks. The 3B model outperforms the Gemma 2 2.6B and Phi 3.5-mini models on tasks such as following instructions, summarization, prompt rewriting, and tool-use, while the 1B is competitive with Gemma.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's the &lt;a href="https://huggingface.co/collections/meta-llama/llama-32-66f448ffc8c32f949b04c8cf"&gt;Llama 3.2 collection&lt;/a&gt; on Hugging Face. You need to accept the new Llama 3.2 Community License Agreement there in order to download those models.&lt;/p&gt;
&lt;p&gt;You can try the four new models out via the &lt;a href="https://lmarena.ai/"&gt;Chatbot Arena&lt;/a&gt; - navigate to "Direct Chat" there and select them from the dropdown menu. You can upload images directly to the chat there to try out the vision features.&lt;/p&gt;
&lt;p&gt;&lt;img alt="llama-3.2-vision-90b-instruct  A photo of two capybaras  Prompt: Describe this photo in detail  The image depicts two capybaras standing in a body of water, with one positioned on the edge of a metal grate. The capybara on the right is partially submerged in the water, while the other is standing on the grate.  Key Features:      Capybaras: Two capybaras are visible in the image.     Water: The capybaras are standing in a body of water, which appears to be murky or dirty.     Metal Grate: A metal grate is visible at the bottom right of the image, with one capybara standing on it.     Background: The background of the image is not clearly visible due to the presence of the water and the capybaras." src="https://static.simonwillison.net/static/2024/capybaras-llama-3.2.jpg" /&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/llama"&gt;llama&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/local-llms"&gt;local-llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vision-llms"&gt;vision-llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/meta"&gt;meta&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ollama"&gt;ollama&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-release"&gt;llm-release&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chatbot-arena"&gt;chatbot-arena&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai"/><category term="generative-ai"/><category term="llama"/><category term="local-llms"/><category term="llms"/><category term="vision-llms"/><category term="meta"/><category term="ollama"/><category term="llm-release"/><category term="files-to-prompt"/><category term="chatbot-arena"/></entry><entry><title>Solving a bug with o1-preview, files-to-prompt and LLM</title><link href="https://simonwillison.net/2024/Sep/25/o1-preview-llm/#atom-tag" rel="alternate"/><published>2024-09-25T18:41:13+00:00</published><updated>2024-09-25T18:41:13+00:00</updated><id>https://simonwillison.net/2024/Sep/25/o1-preview-llm/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://gist.github.com/simonw/03776d9f80534aa8e5348580dc6a800b"&gt;Solving a bug with o1-preview, files-to-prompt and LLM&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I added &lt;a href="https://github.com/simonw/djp/issues/10"&gt;a new feature&lt;/a&gt; to DJP this morning: you can now have plugins specify their middleware in terms of how it should be positioned relative to other middleware - inserted directly before or directly after &lt;code&gt;django.middleware.common.CommonMiddleware&lt;/code&gt; for example.&lt;/p&gt;
&lt;p&gt;At one point I got stuck with a weird test failure, and after ten minutes of head scratching I decided to pipe the entire thing into OpenAI's &lt;code&gt;o1-preview&lt;/code&gt; to see if it could spot the problem. I used &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; to gather the code and &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; to run the prompt:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt &lt;span class="pl-k"&gt;**&lt;/span&gt;/&lt;span class="pl-k"&gt;*&lt;/span&gt;.py -c &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m o1-preview &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;The middleware test is failing showing all of these - why is MiddlewareAfter repeated so many times?&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;['MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware2', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware4', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware2', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware', 'MiddlewareBefore']&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The model whirled away for a few seconds and spat out &lt;a href="https://gist.github.com/simonw/03776d9f80534aa8e5348580dc6a800b#response"&gt;an explanation&lt;/a&gt; of the problem - one of my middleware classes was accidentally calling &lt;code&gt;self.get_response(request)&lt;/code&gt; in two different places.&lt;/p&gt;
&lt;p&gt;I did enjoy how o1 attempted to reference the &lt;a href="https://docs.djangoproject.com/en/5.1/topics/http/middleware/#writing-your-own-middleware"&gt;relevant Django documentation&lt;/a&gt; and then half-repeated, half-hallucinated a quote from it:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Reference: From the Django documentation on writing middleware: Each middleware component is responsible for doing some specific function. They accept the request, do something, and pass the request to the next middleware component (if needed). They can also modify the response before sending it back to the client." src="https://static.simonwillison.net/static/2024/o1-hallucination.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;This took 2,538 input tokens and 4,354 output tokens - &lt;a href="https://gist.github.com/simonw/03776d9f80534aa8e5348580dc6a800b?permalink_comment_id=5207703#gistcomment-5207703"&gt;by my calculations&lt;/a&gt; at $15/million input and $60/million output that prompt cost just under 30 cents.


    &lt;p&gt;Tags: &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/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/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/o1"&gt;o1&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/djp"&gt;djp&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-reasoning"&gt;llm-reasoning&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="ai"/><category term="openai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="o1"/><category term="djp"/><category term="llm-reasoning"/><category term="files-to-prompt"/></entry><entry><title>Jiter</title><link href="https://simonwillison.net/2024/Sep/22/jiter/#atom-tag" rel="alternate"/><published>2024-09-22T20:03:07+00:00</published><updated>2024-09-22T20:03:07+00:00</updated><id>https://simonwillison.net/2024/Sep/22/jiter/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/pydantic/jiter/tree/main/crates/jiter-python"&gt;Jiter&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
One of the challenges in dealing with LLM streaming APIs is the need to parse partial JSON - until the stream has ended you won't have a complete valid JSON object, but you may want to display components of that JSON as they become available.&lt;/p&gt;
&lt;p&gt;I've solved this previously using the &lt;a href="https://pypi.org/project/ijson/"&gt;ijson&lt;/a&gt; streaming JSON library, see &lt;a href="https://til.simonwillison.net/json/ijson-stream"&gt;my previous TIL&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Today I found out about Jiter, a new option from the team behind Pydantic. It's written in Rust and extracted from &lt;a href="https://github.com/pydantic/pydantic-core"&gt;pydantic-core&lt;/a&gt;, so the Python wrapper for it can be installed using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install jiter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can feed it an incomplete JSON bytes object and use &lt;code&gt;partial_mode="on"&lt;/code&gt; to parse the valid subset:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;jiter&lt;/span&gt;
&lt;span class="pl-s1"&gt;partial_json&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;b'{"name": "John", "age": 30, "city": "New Yor'&lt;/span&gt;
&lt;span class="pl-s1"&gt;jiter&lt;/span&gt;.&lt;span class="pl-en"&gt;from_json&lt;/span&gt;(&lt;span class="pl-s1"&gt;partial_json&lt;/span&gt;, &lt;span class="pl-s1"&gt;partial_mode&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"on"&lt;/span&gt;)
&lt;span class="pl-c"&gt;# {'name': 'John', 'age': 30}&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;Or use &lt;code&gt;partial_mode="trailing-strings"&lt;/code&gt; to include incomplete string fields too:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;jiter&lt;/span&gt;.&lt;span class="pl-en"&gt;from_json&lt;/span&gt;(&lt;span class="pl-s1"&gt;partial_json&lt;/span&gt;, &lt;span class="pl-s1"&gt;partial_mode&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"trailing-strings"&lt;/span&gt;)
&lt;span class="pl-c"&gt;# {'name': 'John', 'age': 30, 'city': 'New Yor'}&lt;/span&gt;&lt;/pre&gt;

&lt;p&gt;The &lt;a href="https://github.com/pydantic/jiter/blob/ae5fc7d8548c90ad8762dfdf2ea6461776c2feb6/crates/jiter-python/README.md"&gt;current README&lt;/a&gt; was a little thin, so I submiitted &lt;a href="https://github.com/pydantic/jiter/pull/143"&gt;a PR&lt;/a&gt; with some extra examples. I &lt;a href="https://gist.github.com/simonw/264d487db1a18f8585c2ca0c68e50d1e"&gt;got some help&lt;/a&gt; from &lt;code&gt;files-to-prompt&lt;/code&gt; and Claude 3.5 Sonnet):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;cd crates/jiter-python/ &amp;amp;&amp;amp; files-to-prompt -c README.md tests | llm -m claude-3.5-sonnet --system 'write a new README with comprehensive documentation'&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=41615404#41618393"&gt;jackmpcollins on Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&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/pydantic"&gt;pydantic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="json"/><category term="python"/><category term="rust"/><category term="ai-assisted-programming"/><category term="pydantic"/><category term="files-to-prompt"/></entry><entry><title>files-to-prompt 0.3</title><link href="https://simonwillison.net/2024/Sep/9/files-to-prompt-03/#atom-tag" rel="alternate"/><published>2024-09-09T05:57:35+00:00</published><updated>2024-09-09T05:57:35+00:00</updated><id>https://simonwillison.net/2024/Sep/9/files-to-prompt-03/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/files-to-prompt/releases/tag/0.3"&gt;files-to-prompt 0.3&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New version of my &lt;code&gt;files-to-prompt&lt;/code&gt; CLI tool for turning a bunch of files into a prompt suitable for piping to an LLM, &lt;a href="https://simonwillison.net/2024/Apr/8/files-to-prompt/"&gt;described here previously&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It now has a &lt;code&gt;-c/--cxml&lt;/code&gt; flag for outputting the files in Claude XML-ish notation (XML-ish because it's not actually valid XML) using the format Anthropic describe as &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/long-context-tips#essential-tips-for-long-context-prompts"&gt;recommended for long context&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;files-to-prompt llm-*/README.md --cxml | llm -m claude-3.5-sonnet \
  --system 'return an HTML page about these plugins with usage examples' \
  &amp;gt; /tmp/fancy.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://static.simonwillison.net/static/2024/llm-cxml-demo.html"&gt;Here's what that gave me&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The format itself looks something like this:&lt;/p&gt;
&lt;div class="highlight highlight-text-xml"&gt;&lt;pre&gt;&amp;lt;&lt;span class="pl-ent"&gt;documents&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;document&lt;/span&gt; &lt;span class="pl-e"&gt;index&lt;/span&gt;=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;1&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;source&lt;/span&gt;&amp;gt;llm-anyscale-endpoints/README.md&amp;lt;/&lt;span class="pl-ent"&gt;source&lt;/span&gt;&amp;gt;
&amp;lt;&lt;span class="pl-ent"&gt;document_content&lt;/span&gt;&amp;gt;
# llm-anyscale-endpoints
...
&amp;lt;/&lt;span class="pl-ent"&gt;document_content&lt;/span&gt;&amp;gt;
&amp;lt;/&lt;span class="pl-ent"&gt;document&lt;/span&gt;&amp;gt;
&amp;lt;/&lt;span class="pl-ent"&gt;documents&lt;/span&gt;&amp;gt;&lt;/pre&gt;&lt;/div&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &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/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/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="cli"/><category term="projects"/><category term="tools"/><category term="ai"/><category term="prompt-engineering"/><category term="generative-ai"/><category term="llms"/><category term="anthropic"/><category term="claude"/><category term="files-to-prompt"/></entry><entry><title>json-flatten, now with format documentation</title><link href="https://simonwillison.net/2024/Sep/7/json-flatten/#atom-tag" rel="alternate"/><published>2024-09-07T05:43:01+00:00</published><updated>2024-09-07T05:43:01+00:00</updated><id>https://simonwillison.net/2024/Sep/7/json-flatten/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/json-flatten?tab=readme-ov-file#json-flattening-format"&gt;json-flatten, now with format documentation&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;code&gt;json-flatten&lt;/code&gt; is a fun little Python library I put together a few years ago for converting JSON data into a flat key-value format, suitable for inclusion in an HTML form or query string. It lets you take a structure like this one:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{"foo": {"bar": [1, True, None]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And convert it into key-value pairs like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;foo.bar.[0]$int=1
foo.bar.[1]$bool=True
foo.bar.[2]$none=None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;flatten(dictionary)&lt;/code&gt; function function converts to that format, and &lt;code&gt;unflatten(dictionary)&lt;/code&gt; converts back again.&lt;/p&gt;
&lt;p&gt;I was considering the library for a project today and realized that &lt;a href="https://github.com/simonw/json-flatten/blob/0.3/README.md"&gt;the 0.3 README&lt;/a&gt; was a little thin - it showed how to use the library but didn't provide full details of the format it used.&lt;/p&gt;
&lt;p&gt;On a hunch, I decided to see if &lt;a href="https://simonwillison.net/2024/Apr/8/files-to-prompt/"&gt;files-to-prompt&lt;/a&gt; plus &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; plus Claude 3.5 Sonnet could write that documentation for me. I ran this command:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;files-to-prompt *.py | llm -m claude-3.5-sonnet --system 'write detailed documentation in markdown describing the format used to represent JSON and nested JSON as key/value pairs, include a table as well'&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That &lt;code&gt;*.py&lt;/code&gt; picked up both &lt;code&gt;json_flatten.py&lt;/code&gt; and &lt;code&gt;test_json_flatten.py&lt;/code&gt; - I figured the test file had enough examples in that it should act as a good source of information for the documentation.&lt;/p&gt;
&lt;p&gt;This worked really well! You can see the &lt;a href="https://gist.github.com/simonw/f5caf4ca24662f0078ec3cffcb040ce4#response"&gt;first draft it produced here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It included before and after examples in the documentation. I didn't fully trust these to be accurate, so I gave it this follow-up prompt:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;llm -c "Rewrite that document to use the Python cog library to generate the examples"&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I'm a big fan of &lt;a href="https://nedbatchelder.com/code/cog/"&gt;Cog&lt;/a&gt; for maintaining examples in READMEs that are generated by code. Cog has been around for a couple of decades now so it was a safe bet that Claude would know about it.&lt;/p&gt;
&lt;p&gt;This &lt;a href="https://gist.github.com/simonw/f5caf4ca24662f0078ec3cffcb040ce4#response-1"&gt;almost worked&lt;/a&gt; - it produced valid Cog syntax like the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[[cog
example = {
"fruits": ["apple", "banana", "cherry"]
}

cog.out("```json\n")
cog.out(str(example))
cog.out("\n```\n")
cog.out("Flattened:\n```\n")
for key, value in flatten(example).items():
    cog.out(f"{key}: {value}\n")
cog.out("```\n")
]]]
[[[end]]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But that wasn't entirely right, because it forgot to include the Markdown comments that would hide the Cog syntax, which should have looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- [[[cog --&amp;gt;
...
&amp;lt;!-- ]]] --&amp;gt;
...
&amp;lt;!-- [[[end]]] --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I could have prompted it to correct itself, but at this point I decided to take over and edit the rest of the documentation by hand.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://github.com/simonw/json-flatten/blob/78c2835bf3b7b7cf068fca04a6cf341347dfa2bc/README.md"&gt;end result&lt;/a&gt; was documentation that I'm really happy with, and that I probably wouldn't have bothered to write if Claude hadn't got me started.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&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/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&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/claude-3-5-sonnet"&gt;claude-3-5-sonnet&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;



</summary><category term="json"/><category term="projects"/><category term="ai"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="anthropic"/><category term="claude"/><category term="claude-3-5-sonnet"/><category term="files-to-prompt"/></entry><entry><title>Language models on the command-line</title><link href="https://simonwillison.net/2024/Jun/17/cli-language-models/#atom-tag" rel="alternate"/><published>2024-06-17T16:44:39+00:00</published><updated>2024-06-17T16:44:39+00:00</updated><id>https://simonwillison.net/2024/Jun/17/cli-language-models/#atom-tag</id><summary type="html">
    &lt;p&gt;I gave a talk about accessing Large Language Models from the command-line last week as part of the &lt;a href="https://maven.com/parlance-labs/fine-tuning"&gt;Mastering LLMs: A Conference For Developers &amp;amp; Data Scientists&lt;/a&gt; six week long online conference. The talk focused on my &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; Python command-line utility and ways you can use it (and &lt;a href="https://llm.datasette.io/en/stable/plugins/index.html"&gt;its plugins&lt;/a&gt;) to explore LLMs and use them for useful tasks.&lt;/p&gt;

&lt;p&gt;The talk was recorded and is available &lt;a href="https://www.youtube.com/watch?v=QUXQNi6jQ30"&gt;on YouTube&lt;/a&gt;. Here I've turned it into an &lt;a href="https://simonwillison.net/tags/annotatedtalks/"&gt;annotated presentation&lt;/a&gt;, with detailed notes and screenshots (there were no slides) to accompany the video.&lt;/p&gt;

&lt;iframe style="max-width: 100%" width="560" height="315" src="https://www.youtube-nocookie.com/embed/QUXQNi6jQ30" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen="allowfullscreen"&gt; &lt;/iframe&gt;

  &lt;p style="margin-top: 1em"&gt;&lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; is a tool I started building last year to help run LLM prompts directly from a command-line terminal. Instructions for installing it &lt;a href="https://llm.datasette.io/en/stable/setup.html"&gt;are here&lt;/a&gt; - you can use &lt;code&gt;pipx install llm&lt;/code&gt; or &lt;code&gt;pip install llm&lt;/code&gt; or &lt;code&gt;brew install llm&lt;/code&gt;.&lt;/p&gt;

&lt;div class="llm-slide" id="frame_000003.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_000003.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_000003.jpg" alt="The LLM website" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_000003.jpg"&gt;#&lt;/a&gt;

&lt;p&gt;Once installed you can use it with OpenAI models by running &lt;code&gt;llm keys set openai&lt;/code&gt; and pasting in your OpenAI key - or &lt;a href="https://llm.datasette.io/en/stable/plugins/directory.html#plugin-directory"&gt;install plugins&lt;/a&gt; to use models by other providers, including models you can run locally.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_000259.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_000259.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_000259.jpg" alt="$ llm &amp;#39;five great names for a pet pelican&amp;#39;
Sure, here are five great names for a pet pelican:
1. **Splash**
2. **Captain Beak**
3. **Seabreeze**
4. **Marina**
5. **Pelicano**
These names incorporate elements of the pelican&amp;#39;s natural habitat and distinctive features, adding a fun and fitting touch for your feathered friend!" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_000259.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Basic usage is very simple: once you've configured your OpenAI key, you can run prompts against their models like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm 'five great names for a pet pelican'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output will stream to your terminal, or you can redirect it to a file like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm 'five great names for a pet pelican' &amp;gt; pelicans.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_000341.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_000341.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_000341.jpg" alt="$ llm -c &amp;#39;now do walruses&amp;#39;
Of course, here are five fitting names for a pet walrus:
1. **Whiskers**
2. **Tusk**
3. **Gustav**
4. **Blubber**
5. **Wally**

$ llm -c &amp;#39;justify those!&amp;#39;

Certainly! Here&amp;#39;s why each name suits a pet walrus:
1. **Whiskers**
- **Justification:** Walruses have distinctive, prominent whiskers (vibrissae) that the
y use to detect food on the ocean floor. This name highlights one of their most characteristic features.
2. **Tusk**
- **Justification:** Walruses are known for their long, formidable tusks, which they use for various purposes" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_000341.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;You can use the &lt;code&gt;-c&lt;/code&gt; (for continue) option to send follow-up prompts as part of the same ongoing conversation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm -c 'now do walruses'
# ...
llm -c justify those!'
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_000506.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_000506.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_000506.jpg" alt="Screenshot of the list of Remote APIs plugins in the LLM plugins directory." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_000506.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;LLM supports additional models via plugins. These are listed in the &lt;a href="https://llm.datasette.io/en/stable/plugins/directory.html"&gt;LLM plugins directory&lt;/a&gt;, with dozens of plugins for both remote API-hosted models as well as models you can run directly on your own computer.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_000704.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_000704.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_000704.jpg" alt="$ llm models
Outputs a list of OpenAI models followed by several anthropic models.

$ llm -m claude-3-haiku-20240307 &amp;#39;say hi in spanish with a flourish&amp;#39;
¡Hola, mi amigo! 🌟" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_000704.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Here I'm using the &lt;a href="https://github.com/simonw/llm-claude-3"&gt;llm-claude-3&lt;/a&gt; plugin, which provides access to the Anthropic &lt;a href="https://www.anthropic.com/news/claude-3-family"&gt;Claude 3 family&lt;/a&gt; of models.&lt;/p&gt;
&lt;p&gt;I really like these models. Claude 3 Opus is about equivalent to GPT-4o in terms of quality. Claude 3 Haiku is both cheaper and better than GPT-3.5, and can handle 100,000 input tokens including images.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;llm install llm-claude-3
llm keys set claude
# &amp;lt;Paste key here&amp;gt;
# Now list available models
llm models
# Then run a prompt
llm -m claude-3-haiku-20240307 'say hi in spanish with a flourish'
# Or use the haiku alias
llm -m haiku 'say hi in spanish with a flourish'
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_000818.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_000818.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_000818.jpg" alt="$ llm logs path
/Users/simon/Library/Application Support/io.datasette.llm/logs.db

$ datasette &amp;quot;$(llm logs path)&amp;quot;
...
Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_000818.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Every prompt and response run through the LLM tool is permanently logged to a SQLite database, &lt;a href="https://llm.datasette.io/en/stable/logging.html"&gt;as described here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This command shows the path to that database:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm logs path
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you install &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; you can use it to browse your SQLite database like this, using a terminal trick where the output of one command is passed to another (with double quotes to avoid any problems caused by the space in the directory name):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette "$(llm logs path)"
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_000832.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_000832.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_000832.jpg" alt="Datasette showing the 2,434 responses I have logged as a table, with a search interface at the top." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_000832.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Here's my searchable database of 2,434 responses I've logged from using LLM on my laptop, running in Datasette.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_000853.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_000853.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_000853.jpg" alt="Datasette screenshot showing a conversation, with 3 linked responses" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_000853.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Earlier we ran a prompt and then sent two follow-up prompts to it using the &lt;code&gt;llm -c&lt;/code&gt; option. Those are stored in the database as three responses that are part of the same conversation.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_000924.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_000924.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_000924.jpg" alt="This data as json, copyable, CSV (advanced)

Suggested facets: datetime_utc (date)

model &amp;gt;30

- gpt-3.5-turbo 383
- claude-3-opus-20240229 334
- gpt-4 233
- gpt-4-1106-preview 134
- claude-2 117
- mic-chat-Llama-2-7b-chat-hf-q 4f16 1 93
- mistral-7b-instruct-vo 78
- mic-chat-Llama-2-13b-chat-hf- 94f16 1 74
- claude-3-haiku-20240307 49
- gpt-3.5-turbo-instruct 49
- gpt-40 37
- mic-chat-Mistral-7B-Instruct-v 0.2-q3f16 1 35
- Phi-3-mini-4k-instruct 33
- gemini-pro 33
- gemini-1.5-pro-latest 32
- gguf 28
- gpt-4-turbo-preview 27
- gpt-3.5-turbo-16k 26
- mistral-medium 26
- llama-2-7b-chat.ggmlv3.g8 0 2
- mistral-tiny 23
- Meta-Llama-3-8B-Instruct 21
- llamafile 21
- llama-2-13b.Q8 0 20
- orca-mini-3b 19
- command-r 14
- llama-2-7b.ggmlv3.8 0 14
- mistral-small 14
- mistral-7b-instruct-v0.1.08 0 1 3
- claude 12" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_000924.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Datasette supports facets, which provide a quick overview of unique value counts within the data. I've used GPT-3.5 turbo 383 times, Claude 3 Opus 334 times and a whole bunch of other models.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_000940.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_000940.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_000940.jpg" alt="6 rows where search matches &amp;quot;pelican&amp;quot; and model = &amp;quot;claude-3-opus-20240229&amp;quot; sorted by id descending" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_000940.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Datasette provides search and filtering too - here are the prompts I've run against Claude 3 Opus that match "pelican".&lt;/p&gt;
&lt;p&gt;I have an experimental feature that lets me pass images to some models using the &lt;code&gt;-i filename.png&lt;/code&gt; option. More on that &lt;a href="https://github.com/simonw/llm/issues/331"&gt;in this issue&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/llm-cmd"&gt;llm-cmd&lt;/a&gt; is an example of a plugin that adds an extra sub-command to LLM - in this case one that takes a description of a task and turns that into a pre-populated shell command. I wrote more about that in &lt;a href="https://simonwillison.net/2024/Mar/26/llm-cmd/"&gt;llm cmd undo last git commit—a new plugin for LLM&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_001335.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_001335.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_001335.jpg" alt="Screenshot of the GPT4All website: A free-to-use, locally running, privacy-aware chatbot. No GPU or internet required." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_001335.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;There are plenty of ways to run local models using LLM. One of my favourite is with the &lt;a href="https://github.com/simonw/llm-gpt4all"&gt;llm-gpt4all&lt;/a&gt; plugin, which builds on top of the excellent &lt;a href="https://gpt4all.io/index.html"&gt;GPT4All&lt;/a&gt; Python library by Nomic AI.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_001450.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_001450.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_001450.jpg" alt="$ llm -m mistral-7b-instruct-v@ &amp;#39;five great names for a pet seagull, with explanations&amp;#39; =3
1. &amp;quot;Sally&amp;quot; - This name is perfect for a pet seagull because it&amp;#39;s short and easy to remember,
yet still has a charming and friendly vibe. It also evokes images of a playful and adventuro
us bird that loves to explore the world around her.
2. &amp;quot;Captain Jack&amp;quot; - For any seafaring seagull who loves adventure and exploration, this name
is perfect. Captain Jack suggests a sense of independence and leadership, which could be fitt
ing for a bird who enjoys flying high and exploring new places.
3. &amp;quot;Scarlett&amp;quot; - This name is bold and eye-catching, much like the vibrant feathers of a seagull. Scarlett also has a fiery energy that could reflect the spirit of your adventurous pet.
4. &amp;quot;Nautica&amp;quot; - Nautica means &amp;quot;sailor&amp;quot; in Latin, making it an excellent choice for a seagull

Activity Monitor is also visible, showing a Python process using 350MB of RAM." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_001450.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;To install that plugin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm install llm-gpt4all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then &lt;code&gt;llm models&lt;/code&gt; to list the new models. Each model will be downloaded the first time you try running a prompt through it.&lt;/p&gt;
&lt;p&gt;I used this to run Mistral-7B Instruct - an extremely high quality small (~4GB) model:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm -m mistral-7b-instruct-v0 'five great names for a pet seagull, with explanations'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can run Activity Monitory to see the resources the model is using.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_001544.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_001544.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_001544.jpg" alt="$ llm chat -m mistral-7b-instruct-v0
Chatting with mistral-7b-instruct-v@
Type &amp;#39;exit&amp;#39; or &amp;#39;quit&amp;#39; to exit
Type &amp;#39;!multi&amp;#39; to enter multiple lines, then &amp;#39;!end&amp;#39; to finish
&amp;gt; say hello in spanish

Hola! How can I assist you today?
&amp;gt; now in french
Bonjour! Comment puis-je vous aider aujourd&amp;#39;hui?" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_001544.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Running prompts like this is inefficient, because it loads the full model into memory, runs the prompt and then shuts down the program again.&lt;/p&gt;
&lt;p&gt;Instead, you can use the &lt;code&gt;llm chat&lt;/code&gt; command which keeps the model in memory across multiple prompts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm chat -m mistral-7b-instruct-v0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another option is to run &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt;, which runs its own local server hosting models. The &lt;a href="https://github.com/taketwo/llm-ollama"&gt;llm-ollama&lt;/a&gt; plugin can then be used to run prompts through Ollama from LLM.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_002013.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_002013.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_002013.jpg" alt="The llamafile website" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_002013.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Another really exciting option for running models is &lt;a href="https://github.com/Mozilla-Ocho/llamafile"&gt;llamafile&lt;/a&gt;, a project sponsored by Mozilla that uses Justine Tunney's &lt;a href="https://justine.lol/cosmo3/"&gt;Cosmopolitan&lt;/a&gt; to compile a binary that bundles both a model and the software needed to serve it, in a single file that can execute on several different operating systems.&lt;/p&gt;
&lt;p&gt;I've written more about that here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2023/Nov/29/llamafile/"&gt;llamafile is the new best way to run a LLM on your own computer
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/cosmopolitan/ecosystem"&gt;Catching up with the Cosmopolitan ecosystem
&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_002521.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_002521.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_002521.jpg" alt="Screenshot of a browser window titled llama.cpp showing a photograph

User: Describe image

Llama: The image features a person sitting in a chair next to an outdoor setting with a rooster nearby. A white bowl filled with eggs is placed on the ground, and it appears that the person may be preparing or collecting them from their backyard chicken coop. There are several other birds scattered around the scene as well, adding more life to this" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_002521.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Running &lt;a href="https://llava-vl.github.io/"&gt;LLaVA&lt;/a&gt; using a Llamafile is particularly fun - it's an openly licensed model that can accept images as input as well. It's pretty amazing the results you can get from that, running as a single binary on your laptop.&lt;/p&gt;
&lt;p&gt;Grab that from &lt;a href="https://huggingface.co/Mozilla/llava-v1.5-7b-llamafile"&gt;Mozilla/llava-v1.5-7b-llamafile&lt;/a&gt; on Hugging Face.&lt;/p&gt;
&lt;p&gt;LLM can talk to Llamafile instances via the &lt;a href="https://github.com/simonw/llm-llamafile"&gt;llm-llamafile&lt;/a&gt; plugin.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_002636.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_002636.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_002636.jpg" alt="#!/bin/bash

# Validate that the first argument is an integer
if [[ ! $1 =~ ^[0-9]+$ ]]; then
  echo &amp;quot;Please provide a valid integer as the first argument.&amp;quot;
  exit 1
fi

id=&amp;quot;$1&amp;quot;

# Parse the optional -m argument
model=&amp;quot;haiku&amp;quot;
if [[ $2 == &amp;quot;-m&amp;quot; &amp;amp;&amp;amp; -n $3 ]]; then
  model=&amp;quot;$3&amp;quot;
fi

# Make API call, parse and summarize the discussion
curl -s &amp;quot;https://hn.algolia.com/api/v1/items/$id&amp;quot; | \
  jq -r &amp;#39;recurse(.children[]) | .author + &amp;quot;: &amp;quot; + .text&amp;#39; | \
  llm -m &amp;quot;$model&amp;quot; -s &amp;#39;Summarize the themes of the opinions expressed here.
  For each theme, output a markdown header.
  Include direct &amp;quot;quotations&amp;quot; (with author attribution) where appropriate.
  You MUST quote directly from users when crediting them, with double quotes.
  Fix HTML entities. Output markdown. Go long.&amp;#39;" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_002636.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Now that we can run prompts from our terminal, we can start assembling software by writing scripts.&lt;/p&gt;
&lt;p&gt;Here's a Bash script I wrote to summarize conversations on Hacker News, using longer context models such as Claude 3 Haiku or Google Gemini 1.5 or GPT-4o.&lt;/p&gt;
&lt;p&gt;I wrote more about this in &lt;a href="https://til.simonwillison.net/llms/claude-hacker-news-themes"&gt;Summarizing Hacker News discussion themes with Claude and LLM&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_002850.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_002850.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_002850.jpg" alt="$ hn-summary.sh 40651054 -m 4o

# Dependency and Linking in Software Distribution
Several users discussed issues around dependency management and linking in software distribut
ion:
### Stability and Compatibility Issues
- **e63f67dd-@65b:** &amp;quot;It’s just a classic dependency issue... a combination of ABI instabilit
y and just plain version incompatibility from one distro to the next... My opinion is the opp
osite: I think the old paradigm of distros managing a giant set of system libraries is a bad
one.&amp;quot; —|
- **umanwizard:** &amp;quot;Some distros do actually break out rust dependencies into separate package
s (e.g. Guix does this).&amp;quot;
### Dynamic vs Static Linking
- **jiripospisil:** &amp;quot;Until there&amp;#39;s a vulnerability in one of the dependencies and now you hav
e to rebuild all of the packages which use it... For Rust, there&amp;#39;s also the fact that most pr
ojects use a lock file.&amp;quot;
- **pglata:** &amp;quot;Static linking is essentially easier for people who don&amp;#39;t want to care. But ig
noring security does not mean it solves it" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_002850.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;The script works by hitting the Hacker News Algolia API to return the full, nested JSON structure of the conversation (e.g. &lt;a href="https://hn.algolia.com/api/v1/items/40651054"&gt;this JSON endpoint&lt;/a&gt; for &lt;a href="https://news.ycombinator.com/item?id=40651054"&gt;this conversation&lt;/a&gt;), then runs that through &lt;code&gt;jq&lt;/code&gt; to turn it into text, then pipes that into a model using LLM.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003325.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003325.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003325.jpg" alt="Tutorial: Writing a plugin to support a new model

This tutorial will walk you through developing a new plugin for LLM that adds support for a new Large Language Model.

We will be developing a plugin that implements a simple Markov chain to generate words based on an input string..." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003325.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;If you want to contribute to LLM itself, a great way to do that is to write plugins that support new models. I have &lt;a href="https://llm.datasette.io/en/stable/plugins/tutorial-model-plugin.html"&gt;an extensive tutorial&lt;/a&gt; describing how to do that.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003440.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003440.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003440.jpg" alt="$ llm --system &amp;#39;You are a sentient cheesecake&amp;#39; -m gpt-4o --save cheesecake

$ llm chat -t cheesecake
Chatting with gpt-4o
Type &amp;#39;exit&amp;#39; or &amp;#39;quit&amp;#39; to exit
Type &amp;#39;!multi&amp;#39; to enter multiple lines, then &amp;#39;l!end&amp;#39; to finish
&amp;gt; tell me abotu yourself
Hello! I&amp;#39;m a sentient cheesecake, a delightful fusion of creamy textures and rich flavors. My main ingredients include cream cheese, sugar, eggs, and a touch of vanilla, all resting on a
buttery graham cracker crust. I&amp;#39;m the perfect balance of sweet and tangy, and I take great pride in bringing joy to those who savor a slice of me. Despite my delicate nature, I have a surprisingly strong personality-and apparently, the ability to converse! Is there anything specific you&amp;#39;d like to know about the life of a sentient cheesecake?" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003440.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Another fun trick is to use templates to save and execute parameterized prompts, &lt;a href="https://llm.datasette.io/en/stable/templates.html"&gt;documented here&lt;/a&gt;. The easiest way to create a template is with the &lt;code&gt;--save&lt;/code&gt; option like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm --system 'You are a sentient cheesecake' -m gpt-4o --save cheesecake
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you can chat with a cheesecake:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm chat -t cheesecake
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003532.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003532.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003532.jpg" alt="$ cat demos.md | llm -s &amp;#39;summary, short&amp;#39;
1. **Terminal Basics**
- Quick reminder on basic terminal commands and piping with “|&amp;quot; and *&amp;gt;&amp;quot;, and using ‘cat’.
2. **Ttok Installation &amp;amp; Demonstration**
- Using “pipx install ttok™ to install “ttok™ and demonstrating its usage.
3. **Installing LLM**
- Installation of LLM using either “brew’ or “pipx&amp;quot;.
4. **Configuring for OpenAI** =
- How to configure LLM for OpenAI.
5. **Running a Prompt**
- Example of running a prompt with LLM.
6. **Installing LLM Plugin: 1lm-claude-3**
- Installing the “llm-claude-3&amp;quot; plugin and running a prompt with it.
7. **Browsing Logs with Datasette**
- Demonstrating browsing logs using Datasette.
8. **Using LLM Command**" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003532.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Being able to pipe content into &lt;code&gt;llm&lt;/code&gt; is a really important feature.&lt;/p&gt;
&lt;p&gt;The simplest way to do this is with &lt;code&gt;cat&lt;/code&gt; to send in files. This command summarizes the content of a provided file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat demos.md | llm -s 'summary, short'
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003552.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003552.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003552.jpg" alt="$ llm -c &amp;#39;no much much much shorter, and in haikus&amp;#39;

Terminal basics,
Commands and piping galore,
Cat reminds us all.

Install ttok first,
Pipx makes it easy-peasy,
Quick demonstration.

LLM install,
Brew or pipx, your choice,
Configured OpenAI." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003552.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Once you've done this you can send follow-up prompts with &lt;code&gt;-c&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm -c 'no much much much shorter, and in haikus'
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003619.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003619.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003619.jpg" alt="Output of files-to-prompt - different files are concatenated together with --- between them followed by their filename" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003619.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;My &lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; command can turn a directory of files into a single prompt, suitable for piping into LLM.&lt;/p&gt;
&lt;p&gt;I wrote more about this, including how I developed it, in &lt;a href="https://simonwillison.net/2024/Apr/8/files-to-prompt/"&gt;Building files-to-prompt entirely using Claude 3 Opus&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003715.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003715.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003715.jpg" alt="Screeshot of the shot-scraper website

A command-line utility for taking automated screenshots of websites

Quick start:

pip install shot-scraper
shot-scraper install
shot-scraper https://github.com/simonw/shot-scraper -h 900
Produces this screenshot in a file called github-com-simonw-shot-scraper.png

" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003715.jpg"&gt;#&lt;/a&gt;
 &lt;p&gt;Another tool I frequently use with LLM is &lt;a href="https://shot-scraper.datasette.io/"&gt;shot-scraper&lt;/a&gt; - my command-line tool for screenshotting and scraping websites.&lt;/p&gt;  
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003736.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003736.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003736.jpg" alt="shot-scraper JavaScript command documentation 
" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003736.jpg"&gt;#&lt;/a&gt;

&lt;p&gt;&lt;code&gt;shot-scraper&lt;/code&gt; can &lt;a href="https://shot-scraper.datasette.io/en/stable/javascript.html"&gt;run JavaScript directly against a page&lt;/a&gt; and output the result back to the terminal, suitable for piping into LLM.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003832.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003832.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003832.jpg" alt="Screenshot of Google search results, with the inspector panel open to show the structure of the HTML." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003832.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Google &lt;em&gt;hate&lt;/em&gt; being scraped. Let's use it to scrape Google.&lt;/p&gt;
&lt;p&gt;Google search results have a structure where each link on the page is an &lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt; element wrapped in a link.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003844.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003844.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003844.jpg" alt="shot-scraper javascript &amp;#39;https://www.google.com/search?q=nytimes+slop&amp;#39; &amp;#39;
Array.from(
  document.querySelectorAll(&amp;quot;h3&amp;quot;),
  el =&amp;gt; ({href: el.parentNode.href, title: el.innerText})
)&amp;#39;" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003844.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;We can scrape that using the following terminal command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shot-scraper javascript 'https://www.google.com/search?q=nytimes+slop' '
Array.from(
  document.querySelectorAll("h3"),
  el =&amp;gt; ({href: el.parentNode.href, title: el.innerText})
)'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will load up the search results page in an invisible browser, then execute JavaScript that extracts the results and returns them as JSON.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003856.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003856.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003856.jpg" alt="R T R - A =4 [oBE IR W&amp;#39; R TR |
£ Searc =
= =g _
06_jun_lim — -2sh — 93x24 Rt - .
i 8
&amp;quot;href&amp;quot;: &amp;quot;http://www.nytimes.com/1896/07/31/archives/to-slop.html&amp;quot;, i
“title&amp;quot;: &amp;quot;To Slop.&amp;quot;
i
{
&amp;quot;href&amp;quot;: &amp;quot;https://www.nytimes.com/section/technology”,
&amp;quot;title&amp;quot;: &amp;quot;Technology&amp;quot;
i
1
&amp;quot;href&amp;quot;: &amp;quot;https://www.linkedin.com/posts/luigiraymontanez_slop-is-the-new-name-for-unw
anted-ai-generated-activity-7195821255621574658-MQWK&amp;quot;,
&amp;quot;title&amp;quot;: &amp;quot;Slop is the new name for unwanted AI-generated content”
i
{ e
&amp;quot;href&amp;quot;: &amp;quot;https://www.google.com/search?g=nytimes+slop&amp;amp;sca_esv=a22b124cb26f8e73&amp;amp;ei=F_B
0ZpLQHfet@PEPttSubA4&amp;amp;start=10&amp;amp;sa=N&amp;quot;,
&amp;quot;title&amp;quot;: &amp;quot;More results&amp;quot;
i
o
BTG
&amp;quot;title&amp;quot;: &amp;quot;Try again&amp;quot;
}
1
3
= 7,‘. 54 function findParentWithHveid(element) {
L85 4 __while (element 8§ lelement hasAttribute =
®0A0®17 WO  sSpaces:4 UTF-8 LF Markdown &amp;amp; A17Spell [
" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003856.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;The results as JSON include the &lt;code&gt;href&lt;/code&gt; and &lt;code&gt;title&lt;/code&gt; of each of those search results.&lt;/p&gt;
&lt;p&gt;We could send that to LLM, but I'd like to grab the search snippets as well.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003914.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003914.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003914.jpg" alt="Screenshot showing the command with the more complex JavaScript." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003914.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Here's the more complex recipe that also fetches the search snippets visible on the page:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shot-scraper javascript 'https://www.google.com/search?q=nytimes+slop' '
() =&amp;gt; {
    function findParentWithHveid(element) {
        while (element &amp;amp;&amp;amp; !element.hasAttribute("data-hveid")) {
            element = element.parentElement;
        }
        return element;
    }
    return Array.from(
        document.querySelectorAll("h3"),
        el =&amp;gt; findParentWithHveid(el).innerText
    );
}' | llm -s 'describe slop'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the end it pipes them into LLM with instructions to use that context to "describe slop".&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_003931.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_003931.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_003931.jpg" alt="The term &amp;quot;slop&amp;quot; has recently emerged in the context of artificial intelligence to describe questionable or low-quality AI-generated material. This term appears to be gaining traction among tech and media commentators, reflecting growing concerns about the proliferation of such content." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_003931.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;... and it works! We get back an answer from the LLM that summarizes the search results that we just scraped.&lt;/p&gt;
&lt;p&gt;We have implemented basic RAG - Retrieval Augmented Generation, where search results are used to answer a question - using a terminal script that scrapes search results from Google and pipes them into an LLM.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_004133.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_004133.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_004133.jpg" alt="Output of llm --help showing a list of commands that includes cmd, embed, embed-models and embed-multi." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_004133.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;Speaking of RAG... a common technique for implementing that pattern is to take advantage of embeddings and vector search to find content that is semantically similar to the user's question, without necessarily matching on exact keywords.&lt;/p&gt;
&lt;p&gt;I wrote an extensive introduction to embeddings in &lt;a href="https://simonwillison.net/2023/Oct/23/embeddings/"&gt;Embeddings: What they are and why they matter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;LLM includes support for calculating, storing and searching embeddings through its &lt;code&gt;llm embed-models&lt;/code&gt;, &lt;code&gt;llm embed&lt;/code&gt; and &lt;code&gt;llm embed-multi&lt;/code&gt; commands, &lt;a href="https://llm.datasette.io/en/stable/embeddings/index.html"&gt;documented here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;llm embed-models&lt;/code&gt; command lists currently available embedding models - the OpenAI models plus any that have been added by plugins.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_004239.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_004239.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_004239.jpg" alt="A huge JSON array of floating point numbers, followed by:

$ llm embed -m 3-small -c &amp;#39;hello there&amp;#39; | jq length

1536" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_004239.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;The &lt;code&gt;llm embed&lt;/code&gt; command can embed content. This will output a list of floating point numbers for the specified content, using the OpenAI 3-small embedding model.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm embed -m 3-small -c "hello there"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add &lt;code&gt;-f hex&lt;/code&gt; to get that out as hexadecimal. Neither of these formats are particularly useful on their own!&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_004332.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_004332.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_004332.jpg" alt="Screenshot of the llm embed-multi documentation." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_004332.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;The &lt;a href="https://llm.datasette.io/en/stable/embeddings/cli.html#llm-embed-multi"&gt;llm embed-multi&lt;/a&gt; command is much more useful. It can run embeddings against content in bulk - from a CSV or JSON file, from a directory full of content or even from a SQLite database. Those embedding vectors will be stored in SQLite ready to be used for search or similarity queries.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_004418.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_004418.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_004418.jpg" alt="Screenshot of my blog&amp;#39;s collection of 7178 blogmarks" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_004418.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;I collect bookmarks (I call them "blogmarks") on my blog - links with short descriptions. I have &lt;a href="https://simonwillison.net/search/?type=blogmark"&gt;over 7,178 of them&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let's create embeddings for all of those using LLM.&lt;/p&gt;
&lt;p&gt;I used the SQLite database version of my blog available from &lt;code&gt;https://datasette.simonwillison.net/simonwillisonblog.db&lt;/code&gt; (a 90MB file).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_004424.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_004424.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_004424.jpg" alt="Screenshot of the blog_blogmark table in Datasette" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_004424.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;The content lives in the &lt;a href="https://datasette.simonwillison.net/simonwillisonblog/blog_blogmark"&gt;blog_blogmark&lt;/a&gt; SQLite table, which I can explore using Datasette (in this case the &lt;a href="https://datasette.io/desktop"&gt;Datasette Desktop&lt;/a&gt; macOS Electron app).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_004444.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_004444.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_004444.jpg" alt="Custom SQL query returning more than 2,000 rows 

select id, link_url, link_title, commentary from blog_blogmark" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_004444.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;First step is to compose a SQL query returning the data we want to embed. The first column will be treated as a unique identifier to store along with the embedding vector, and any subsequent columns will be used as input to the embedding model.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select id, link_url, link_title, commentary from blog_blogmark
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_004502.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_004502.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_004502.jpg" alt="R I R S P Y @ B e A C m 7 Qg
06_jun_lim — -zsh — 93x24 R ERCTT

$ llm embed-multi links \
-d simonwillisonblog.db \
--sql &amp;#39;select id, link_url, link_title, commentary from blog_blogmark&amp;#39; \
-m 3-small --store" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_004502.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;I can run this command to embed all of the content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm embed-multi links \
  -d simonwillisonblog.db \
  --sql 'select id, link_url, link_title, commentary from blog_blogmark' \
  -m 3-small --store
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will create an embedding collection called "links". It will run the SQL query we created before, using the OpenAI &lt;code&gt;3-small&lt;/code&gt; model. The &lt;code&gt;--store&lt;/code&gt; link means it will store a copy of the text in the database as well - without that it would just store identifiers and we would need to use those to look up the text later on when running queries.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_004521.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_004521.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_004521.jpg" alt="Progress bar - 4% done, 00:01:18" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_004521.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;The &lt;code&gt;llm embed-multi&lt;/code&gt; command shows a progress bar for how far it has got.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_004539.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_004539.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_004539.jpg" alt="Screenshot of the embeddings table in Datasette, with a bunch of binary data visible." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_004539.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;While the command is running we can use Datasette to watch as the &lt;code&gt;embeddings&lt;/code&gt; table is filled with data - one row for each of the items we are embedding, each storing a big ugly binary blob of data representing the embedding vector (in &lt;a href="https://llm.datasette.io/en/stable/embeddings/storage.html"&gt;this storage format&lt;/a&gt;).&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_004720.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_004720.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_004720.jpg" alt="$ llm similar links -c &amp;#39;things that make me angry&amp;#39; -d simonwillisonblog.db
{&amp;quot;1&amp;quot;: &amp;quot;448&amp;quot;, &amp;quot;score&amp;quot;: 8.31105587659133327, “content”: “http://www.russellbeattie.com/notebook/1006697.html Overreaction Absolutely storming rant from Russell about America&amp;#39;s culture of fear.&amp;quot;, &amp;quot;metodata”: null}
..." /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_004720.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;And now we can run searches! This command finds everything in the "links" collection that is most similar to the search term "things that make me angry":&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;llm similar links -c 'things that make me angry' -d simonwillisonblog.db
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_004824.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_004824.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_004824.jpg" alt="cat ~/.local/bin/blog-answer.sh

#!/bin/bash

# Check if a query was provided
if [ &amp;quot;$#&amp;quot; -ne 1 ]; then
    echo &amp;quot;Usage: $0 &amp;#39;Your query&amp;#39;&amp;quot;
    exit 1
fi

llm similar blog-paragraphs -c &amp;quot;query: $1&amp;quot; \
  | jq &amp;#39;.content | sub(&amp;quot;passage: &amp;quot;; &amp;quot;&amp;quot;)&amp;#39; -r \
  | llm -m llamafile \
  &amp;quot;$1&amp;quot; -s &amp;#39;You answer questions as a single paragraph&amp;#39;

# | llm -m mlc-chat-Llama-2-7b-chat-hf-q4f16_1 \
# /Users/simon/.local/share/virtualenvs/llm-mlc-SwKbovmI/bin/llm -m mlc-chat-Llama-2-7b-chat-hf-q4f16_1
" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_004824.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;We can implement another version of RAG on top of this as well, by finding similar documents to our search term and then piping those results back into LLM to execute a prompt.&lt;/p&gt;
&lt;p&gt;I wrote more about this in &lt;a href="https://til.simonwillison.net/llms/embed-paragraphs"&gt;Embedding paragraphs from my blog with E5-large-v2&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_005209.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_005209.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_005209.jpg" alt="LLM documentation: OpenAl-compatible models" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_005209.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;During the Q&amp;amp;A I mentioned that LLM can talk to anything that provides an OpenAI-compatible API endpoint using just configuration, no extra code. That's &lt;a href="https://llm.datasette.io/en/stable/other-models.html#openai-compatible-models"&gt;described in the documentation here&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_005333.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_005333.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_005333.jpg" alt="Screenshot of the Python API documentation" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_005333.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;I also showed LLM's &lt;a href="https://llm.datasette.io/en/stable/python-api.html"&gt;Python API documentation&lt;/a&gt;, but warned that this is not yet considered stable as I'm not 100% happy with how this API works yet.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="llm-slide" id="frame_010311.jpg"&gt;
  &lt;a class="slide-link" href="https://static.simonwillison.net/static/2024/llm/frame_010311.jpg"&gt;&lt;img src="https://static.simonwillison.net/static/2024/llm/frame_010311.jpg" alt="GitHub Issue with 77 comments: Figure out how to serve an AWS Lambda function with a Function URL from a custom subdomain" /&gt;&lt;/a&gt;
  &lt;a style="float: right; padding-left: 1em; border: none" href="https://simonwillison.net/2024/Jun/17/cli-language-models/#frame_010311.jpg"&gt;#&lt;/a&gt;
  &lt;p&gt;The final question from the audience was about my personal productivity.&lt;/p&gt;
&lt;p&gt;I wrote a bit about this a few years ago in &lt;a href="https://simonwillison.net/2022/Nov/26/productivity/"&gt;Coping strategies for the serial project hoarder&lt;/a&gt; - how I use extensive documentation and unit tests to allow me to work on way more projects at once by ensuring I don't have to remember the details of any of them.&lt;/p&gt;
&lt;p&gt;My other trick is that I tend to pick projects that fit my unique combination of previous experiences. I built LLM because I already had experience with LLM APIs, Python CLI tools (using &lt;a href="https://click.palletsprojects.com/"&gt;Click&lt;/a&gt;) and plugin systems (using &lt;a href="https://pluggy.readthedocs.io/"&gt;Pluggy&lt;/a&gt;). As a result I happened to be one of the best positioned people in the world to build a plugin-based CLI tool for working with LLMs!&lt;/p&gt;
&lt;/div&gt;

&lt;h4 id="llm-colophon"&gt;Colophon&lt;/h4&gt;
&lt;p&gt;Here's how I turned the YouTube video of this talk into an annotated presentation:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I downloaded a &lt;code&gt;.mp4&lt;/code&gt; version of the talk from YouTube using &lt;a href="https://github.com/yt-dlp/yt-dlp"&gt;yt-dlp&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;I ran that through &lt;a href="https://goodsnooze.gumroad.com/l/macwhisper"&gt;MacWhisper&lt;/a&gt; to create my own transcript for copying extracts from into my write-up - although this time I didn't end up using any of the transcript text.&lt;/li&gt;
&lt;li&gt;I played the video (at 2x speed) in QuickTime Player and used the &lt;code&gt;capture.sh&lt;/code&gt; script &lt;a href="https://til.simonwillison.net/macos/quicktime-capture-script"&gt;described here&lt;/a&gt; to grab screenshots of the individual interesting frames that I wanted to use for my post.&lt;/li&gt;
&lt;li&gt;I loaded those screenshots into &lt;a href="https://til.simonwillison.net/tools/annotated-presentations"&gt;my annotated presentation tool&lt;/a&gt; (which I &lt;a href="https://simonwillison.net/2023/Aug/6/annotated-presentations/"&gt;described in this post&lt;/a&gt;) and used that to run OCR against them for alt text and to add commentary to accompany each screenshot.&lt;/li&gt;
&lt;li&gt;I assembled the result into this finished blog entry, adding intro text and the YouTube embed as HTML.&lt;/li&gt;
&lt;/ol&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&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/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&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/local-llms"&gt;local-llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&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/llamafile"&gt;llamafile&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ollama"&gt;ollama&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/macwhisper"&gt;macwhisper&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="cli"/><category term="projects"/><category term="my-talks"/><category term="ai"/><category term="datasette"/><category term="openai"/><category term="generative-ai"/><category term="local-llms"/><category term="llms"/><category term="llm"/><category term="anthropic"/><category term="annotated-talks"/><category term="llamafile"/><category term="ollama"/><category term="files-to-prompt"/><category term="macwhisper"/></entry><entry><title>Building files-to-prompt entirely using Claude 3 Opus</title><link href="https://simonwillison.net/2024/Apr/8/files-to-prompt/#atom-tag" rel="alternate"/><published>2024-04-08T20:40:08+00:00</published><updated>2024-04-08T20:40:08+00:00</updated><id>https://simonwillison.net/2024/Apr/8/files-to-prompt/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://github.com/simonw/files-to-prompt"&gt;files-to-prompt&lt;/a&gt; is a new tool I built to help me pipe several files at once into prompts to LLMs such as Claude and GPT-4.&lt;/p&gt;
&lt;p&gt;When combined with my &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; command-line tool it lets you do things like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt README.md files_to_prompt &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m opus \
  --system &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Update this README to reflect this functionality&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I wrote &lt;code&gt;files-to-prompt&lt;/code&gt; almost entirely using &lt;a href="https://www.anthropic.com/news/claude-3-family"&gt;Claude 3 Opus&lt;/a&gt;, &lt;a href="https://github.com/simonw/llm-claude-3"&gt;llm-claude-3&lt;/a&gt; and &lt;code&gt;files-to-prompt&lt;/code&gt; itself, once it was functional enough to be useful.&lt;/p&gt;
&lt;h4 id="building-the-initial-tool"&gt;Building the initial tool&lt;/h4&gt;
&lt;p&gt;I started with my &lt;a href="https://github.com/simonw/click-app"&gt;click-app&lt;/a&gt; cookiecutter template. This can quickly spin up a skeleton of a new Python command-line tool using the &lt;a href="https://click.palletsprojects.com/"&gt;Click&lt;/a&gt; library:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;cookiecutter gh:simonw/click-app
  [1/6] &lt;span class="pl-en"&gt;app_name&lt;/span&gt; (): files-to-prompt
  [2/6] &lt;span class="pl-en"&gt;description&lt;/span&gt; (): Concatenate a directory full of files into a single prompt &lt;span class="pl-k"&gt;for&lt;/span&gt; use with LLMs
  [3/6] hyphenated (files-to-prompt): 
  [4/6] underscored (files_to_prompt): 
  [5/6] &lt;span class="pl-en"&gt;github_username&lt;/span&gt; (): simonw
  [6/6] &lt;span class="pl-en"&gt;author_name&lt;/span&gt; (): Simon Willison&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I opened the &lt;code&gt;files_to_prompt/cli.py&lt;/code&gt; file it created for me and used it to spec out how the initial version should work:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;click&lt;/span&gt;


&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;command&lt;/span&gt;()&lt;/span&gt;
&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;argument&lt;/span&gt;(&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s"&gt;"path"&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s1"&gt;type&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-v"&gt;Path&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-en"&gt;)&lt;/span&gt;
&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;option&lt;/span&gt;(&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s"&gt;"--include-hidden"&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s1"&gt;is_flag&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s1"&gt;help&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"Include files and folders starting with ."&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;)&lt;/span&gt;
&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;version_option&lt;/span&gt;()&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;cli&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;, &lt;span class="pl-s1"&gt;include_hidden&lt;/span&gt;):
    &lt;span class="pl-s"&gt;"""&lt;/span&gt;
&lt;span class="pl-s"&gt;    Takes a path to a folder and outputs every file in that folder,&lt;/span&gt;
&lt;span class="pl-s"&gt;    recursively, each one preceeded with its filename like this:&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;    path/to/file.py&lt;/span&gt;
&lt;span class="pl-s"&gt;    ----&lt;/span&gt;
&lt;span class="pl-s"&gt;    Contents of file.py goes here&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;    ---&lt;/span&gt;
&lt;span class="pl-s"&gt;    path/to/file2.py&lt;/span&gt;
&lt;span class="pl-s"&gt;    ---&lt;/span&gt;
&lt;span class="pl-s"&gt;    ...&lt;/span&gt;
&lt;span class="pl-s"&gt;    """&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;This code defines a tool that takes a positional &lt;code&gt;path&lt;/code&gt; argument and an optional &lt;code&gt;--include-hidden&lt;/code&gt; flag, along with a Python docstring describing what it should do which will be displayed as the &lt;code&gt;--help&lt;/code&gt; output.&lt;/p&gt;
&lt;p&gt;I'd originally intended to actually build the tool... but once I got to this point I realized that this might be enough information for Claude 3 Opus to do the rest of the work for me.&lt;/p&gt;
&lt;p&gt;So I ran this command:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;cat files_to_prompt/cli.py &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m opus \
  --system &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;finish this code for me&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can see the &lt;a href="https://gist.github.com/simonw/3492bc9a6ff639ff7fbaa081fa508131"&gt;full transcript here&lt;/a&gt;, but from this single prompt Claude 3 Opus produced the exact implementation I needed:&lt;/p&gt;
&lt;pre&gt;    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;root&lt;/span&gt;, &lt;span class="pl-s1"&gt;dirs&lt;/span&gt;, &lt;span class="pl-s1"&gt;files&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-en"&gt;walk&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;):
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-s1"&gt;include_hidden&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;dirs&lt;/span&gt;[:] &lt;span class="pl-c1"&gt;=&lt;/span&gt; [&lt;span class="pl-s1"&gt;d&lt;/span&gt; &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;dirs&lt;/span&gt; &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;.&lt;span class="pl-en"&gt;startswith&lt;/span&gt;(&lt;span class="pl-s"&gt;"."&lt;/span&gt;)]
            &lt;span class="pl-s1"&gt;files&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [&lt;span class="pl-s1"&gt;f&lt;/span&gt; &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;files&lt;/span&gt; &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;.&lt;span class="pl-en"&gt;startswith&lt;/span&gt;(&lt;span class="pl-s"&gt;"."&lt;/span&gt;)]

        &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;file&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;files&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;file_path&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;.&lt;span class="pl-en"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;root&lt;/span&gt;, &lt;span class="pl-s1"&gt;file&lt;/span&gt;)
            &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;open&lt;/span&gt;(&lt;span class="pl-s1"&gt;file_path&lt;/span&gt;, &lt;span class="pl-s"&gt;"r"&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;:
                &lt;span class="pl-s1"&gt;file_contents&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;.&lt;span class="pl-en"&gt;read&lt;/span&gt;()

            &lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;echo&lt;/span&gt;(&lt;span class="pl-s1"&gt;file_path&lt;/span&gt;)
            &lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;echo&lt;/span&gt;(&lt;span class="pl-s"&gt;"---"&lt;/span&gt;)
            &lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;echo&lt;/span&gt;(&lt;span class="pl-s1"&gt;file_contents&lt;/span&gt;)
            &lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;echo&lt;/span&gt;()
            &lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;echo&lt;/span&gt;(&lt;span class="pl-s"&gt;"---"&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;I pasted that into by &lt;code&gt;cli.py&lt;/code&gt; module and tried running it... and it worked first time.&lt;/p&gt;
&lt;p&gt;I tried a more challenging prompt next:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;cat files_to_prompt/cli.py &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m opus &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;modify this code to add a --ignore-gitignore flag which defaults toFalse.&lt;/span&gt;
&lt;span class="pl-s"&gt;Without this flag the code should read any .gitignore files and filter&lt;/span&gt;
&lt;span class="pl-s"&gt;content based on the rules within them. With the flag it should not do&lt;/span&gt;
&lt;span class="pl-s"&gt;that.&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;	&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I forgot to use the &lt;code&gt;--system&lt;/code&gt; option here, so the argument prompt I passed to &lt;code&gt;llm&lt;/code&gt; was concatenated directly onto the end of the code rather than being treated as a system/instruction prompt. This turned out not to matter.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.gitignore&lt;/code&gt; is a tricky format to handle. Claude 3 Opus didn't do a comprehensive implementation, but it did support the subset that I cared about for this step of the development process.&lt;/p&gt;
&lt;p&gt;Here's what it gave me (&lt;a href="https://gist.github.com/simonw/437b764862c29d9057c11e5b60695b1b"&gt;full transcript here&lt;/a&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;click&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;pathlib&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Path&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;fnmatch&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;fnmatch&lt;/span&gt;


&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;should_ignore&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;, &lt;span class="pl-s1"&gt;gitignore_rules&lt;/span&gt;):
    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;rule&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;gitignore_rules&lt;/span&gt;:
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-en"&gt;fnmatch&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;, &lt;span class="pl-s1"&gt;rule&lt;/span&gt;):
            &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-c1"&gt;True&lt;/span&gt;
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-c1"&gt;False&lt;/span&gt;


&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;read_gitignore&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;gitignore_path&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;.&lt;span class="pl-en"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;, &lt;span class="pl-s"&gt;".gitignore"&lt;/span&gt;)
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;.&lt;span class="pl-en"&gt;isfile&lt;/span&gt;(&lt;span class="pl-s1"&gt;gitignore_path&lt;/span&gt;):
        &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;open&lt;/span&gt;(&lt;span class="pl-s1"&gt;gitignore_path&lt;/span&gt;, &lt;span class="pl-s"&gt;"r"&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;:
            &lt;span class="pl-k"&gt;return&lt;/span&gt; [&lt;span class="pl-s1"&gt;line&lt;/span&gt;.&lt;span class="pl-en"&gt;strip&lt;/span&gt;() &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;line&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt; &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;line&lt;/span&gt;.&lt;span class="pl-en"&gt;strip&lt;/span&gt;() &lt;span class="pl-c1"&gt;and&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-s1"&gt;line&lt;/span&gt;.&lt;span class="pl-en"&gt;startswith&lt;/span&gt;(&lt;span class="pl-s"&gt;"#"&lt;/span&gt;)]
    &lt;span class="pl-k"&gt;return&lt;/span&gt; []


&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;command&lt;/span&gt;()&lt;/span&gt;
&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;argument&lt;/span&gt;(&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s"&gt;"path"&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s1"&gt;type&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-v"&gt;Path&lt;/span&gt;(&lt;span class="pl-s1"&gt;exists&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;, &lt;span class="pl-s1"&gt;file_okay&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;False&lt;/span&gt;, &lt;span class="pl-s1"&gt;dir_okay&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;, &lt;span class="pl-s1"&gt;readable&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;),&lt;/span&gt;
&lt;span class="pl-en"&gt;)&lt;/span&gt;
&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;option&lt;/span&gt;(&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s"&gt;"--include-hidden"&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s1"&gt;is_flag&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s1"&gt;help&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"Include files and folders starting with ."&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;)&lt;/span&gt;
&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;option&lt;/span&gt;(&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s"&gt;"--ignore-gitignore"&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s1"&gt;is_flag&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;    &lt;span class="pl-s1"&gt;help&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"Ignore .gitignore files and include all files"&lt;/span&gt;,&lt;/span&gt;
&lt;span class="pl-en"&gt;)&lt;/span&gt;
&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;version_option&lt;/span&gt;()&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;cli&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;, &lt;span class="pl-s1"&gt;include_hidden&lt;/span&gt;, &lt;span class="pl-s1"&gt;ignore_gitignore&lt;/span&gt;):
    &lt;span class="pl-s"&gt;"""&lt;/span&gt;
&lt;span class="pl-s"&gt;    Takes a path to a folder and outputs every file in that folder,&lt;/span&gt;
&lt;span class="pl-s"&gt;    recursively, each one preceded with its filename like this:&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;    path/to/file.py&lt;/span&gt;
&lt;span class="pl-s"&gt;    ----&lt;/span&gt;
&lt;span class="pl-s"&gt;    Contents of file.py goes here&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;    ---&lt;/span&gt;
&lt;span class="pl-s"&gt;    path/to/file2.py&lt;/span&gt;
&lt;span class="pl-s"&gt;    ---&lt;/span&gt;
&lt;span class="pl-s"&gt;    ...&lt;/span&gt;
&lt;span class="pl-s"&gt;    """&lt;/span&gt;
    &lt;span class="pl-s1"&gt;gitignore_rules&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [] &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;ignore_gitignore&lt;/span&gt; &lt;span class="pl-k"&gt;else&lt;/span&gt; &lt;span class="pl-en"&gt;read_gitignore&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;)

    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;root&lt;/span&gt;, &lt;span class="pl-s1"&gt;dirs&lt;/span&gt;, &lt;span class="pl-s1"&gt;files&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-en"&gt;walk&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;):
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-s1"&gt;include_hidden&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;dirs&lt;/span&gt;[:] &lt;span class="pl-c1"&gt;=&lt;/span&gt; [&lt;span class="pl-s1"&gt;d&lt;/span&gt; &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;dirs&lt;/span&gt; &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;.&lt;span class="pl-en"&gt;startswith&lt;/span&gt;(&lt;span class="pl-s"&gt;"."&lt;/span&gt;)]
            &lt;span class="pl-s1"&gt;files&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [&lt;span class="pl-s1"&gt;f&lt;/span&gt; &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;files&lt;/span&gt; &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;.&lt;span class="pl-en"&gt;startswith&lt;/span&gt;(&lt;span class="pl-s"&gt;"."&lt;/span&gt;)]

        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-s1"&gt;ignore_gitignore&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;gitignore_rules&lt;/span&gt;.&lt;span class="pl-en"&gt;extend&lt;/span&gt;(&lt;span class="pl-en"&gt;read_gitignore&lt;/span&gt;(&lt;span class="pl-s1"&gt;root&lt;/span&gt;))
            &lt;span class="pl-s1"&gt;dirs&lt;/span&gt;[:] &lt;span class="pl-c1"&gt;=&lt;/span&gt; [&lt;span class="pl-s1"&gt;d&lt;/span&gt; &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;dirs&lt;/span&gt; &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-en"&gt;should_ignore&lt;/span&gt;(&lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;.&lt;span class="pl-en"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;root&lt;/span&gt;, &lt;span class="pl-s1"&gt;d&lt;/span&gt;), &lt;span class="pl-s1"&gt;gitignore_rules&lt;/span&gt;)]
            &lt;span class="pl-s1"&gt;files&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [&lt;span class="pl-s1"&gt;f&lt;/span&gt; &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;files&lt;/span&gt; &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-en"&gt;should_ignore&lt;/span&gt;(&lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;.&lt;span class="pl-en"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;root&lt;/span&gt;, &lt;span class="pl-s1"&gt;f&lt;/span&gt;), &lt;span class="pl-s1"&gt;gitignore_rules&lt;/span&gt;)]

        &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;file&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;files&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;file_path&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;.&lt;span class="pl-en"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;root&lt;/span&gt;, &lt;span class="pl-s1"&gt;file&lt;/span&gt;)
            &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;open&lt;/span&gt;(&lt;span class="pl-s1"&gt;file_path&lt;/span&gt;, &lt;span class="pl-s"&gt;"r"&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;:
                &lt;span class="pl-s1"&gt;file_contents&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;.&lt;span class="pl-en"&gt;read&lt;/span&gt;()

            &lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;echo&lt;/span&gt;(&lt;span class="pl-s1"&gt;file_path&lt;/span&gt;)
            &lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;echo&lt;/span&gt;(&lt;span class="pl-s"&gt;"---"&lt;/span&gt;)
            &lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;echo&lt;/span&gt;(&lt;span class="pl-s1"&gt;file_contents&lt;/span&gt;)
            &lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;echo&lt;/span&gt;()
            &lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-en"&gt;echo&lt;/span&gt;(&lt;span class="pl-s"&gt;"---"&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;From that point on I was hooked: could I use the new &lt;code&gt;files-to-prompt&lt;/code&gt; tool to build the rest of itself?&lt;/p&gt;
&lt;p&gt;Since I'd already bootstrapped it to a point where I could use it, I ran it for real for the first time:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt &lt;span class="pl-c1"&gt;.&lt;/span&gt; &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m opus --system &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;fill out the test_files_to_prompt.py with tests, use tmpdir fixture&lt;/span&gt;
&lt;span class="pl-s"&gt;to create example directories to run the command against - tests&lt;/span&gt;
&lt;span class="pl-s"&gt;should cover both of the optional flags&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here I'm sending it quite a bit of code - the implementation, the README and the Apache 2 License are all included in that prompt.&lt;/p&gt;
&lt;p&gt;It wrote me these tests:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;click&lt;/span&gt;.&lt;span class="pl-s1"&gt;testing&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;CliRunner&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;files_to_prompt&lt;/span&gt;.&lt;span class="pl-s1"&gt;cli&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;cli&lt;/span&gt;

&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;test_basic_functionality&lt;/span&gt;(&lt;span class="pl-s1"&gt;tmpdir&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;runner&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;CliRunner&lt;/span&gt;()
    &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-s1"&gt;tmpdir&lt;/span&gt;.&lt;span class="pl-en"&gt;as_cwd&lt;/span&gt;():
        &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-en"&gt;makedirs&lt;/span&gt;(&lt;span class="pl-s"&gt;"test_dir"&lt;/span&gt;)
        &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;open&lt;/span&gt;(&lt;span class="pl-s"&gt;"test_dir/file1.txt"&lt;/span&gt;, &lt;span class="pl-s"&gt;"w"&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;f&lt;/span&gt;.&lt;span class="pl-en"&gt;write&lt;/span&gt;(&lt;span class="pl-s"&gt;"Contents of file1"&lt;/span&gt;)
        &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;open&lt;/span&gt;(&lt;span class="pl-s"&gt;"test_dir/file2.txt"&lt;/span&gt;, &lt;span class="pl-s"&gt;"w"&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;f&lt;/span&gt;.&lt;span class="pl-en"&gt;write&lt;/span&gt;(&lt;span class="pl-s"&gt;"Contents of file2"&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-s1"&gt;runner&lt;/span&gt;.&lt;span class="pl-en"&gt;invoke&lt;/span&gt;(&lt;span class="pl-s1"&gt;cli&lt;/span&gt;, [&lt;span class="pl-s"&gt;"test_dir"&lt;/span&gt;])
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;exit_code&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"test_dir/file1.txt"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"Contents of file1"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"test_dir/file2.txt"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"Contents of file2"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&lt;/span&gt;

&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;test_include_hidden&lt;/span&gt;(&lt;span class="pl-s1"&gt;tmpdir&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;runner&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;CliRunner&lt;/span&gt;()
    &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-s1"&gt;tmpdir&lt;/span&gt;.&lt;span class="pl-en"&gt;as_cwd&lt;/span&gt;():
        &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-en"&gt;makedirs&lt;/span&gt;(&lt;span class="pl-s"&gt;"test_dir"&lt;/span&gt;)
        &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;open&lt;/span&gt;(&lt;span class="pl-s"&gt;"test_dir/.hidden.txt"&lt;/span&gt;, &lt;span class="pl-s"&gt;"w"&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;f&lt;/span&gt;.&lt;span class="pl-en"&gt;write&lt;/span&gt;(&lt;span class="pl-s"&gt;"Contents of hidden file"&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-s1"&gt;runner&lt;/span&gt;.&lt;span class="pl-en"&gt;invoke&lt;/span&gt;(&lt;span class="pl-s1"&gt;cli&lt;/span&gt;, [&lt;span class="pl-s"&gt;"test_dir"&lt;/span&gt;])
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;exit_code&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"test_dir/.hidden.txt"&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&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-s1"&gt;runner&lt;/span&gt;.&lt;span class="pl-en"&gt;invoke&lt;/span&gt;(&lt;span class="pl-s1"&gt;cli&lt;/span&gt;, [&lt;span class="pl-s"&gt;"test_dir"&lt;/span&gt;, &lt;span class="pl-s"&gt;"--include-hidden"&lt;/span&gt;])
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;exit_code&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"test_dir/.hidden.txt"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"Contents of hidden file"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&lt;/span&gt;

&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;test_ignore_gitignore&lt;/span&gt;(&lt;span class="pl-s1"&gt;tmpdir&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;runner&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;CliRunner&lt;/span&gt;()
    &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-s1"&gt;tmpdir&lt;/span&gt;.&lt;span class="pl-en"&gt;as_cwd&lt;/span&gt;():
        &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-en"&gt;makedirs&lt;/span&gt;(&lt;span class="pl-s"&gt;"test_dir"&lt;/span&gt;)
        &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;open&lt;/span&gt;(&lt;span class="pl-s"&gt;"test_dir/.gitignore"&lt;/span&gt;, &lt;span class="pl-s"&gt;"w"&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;f&lt;/span&gt;.&lt;span class="pl-en"&gt;write&lt;/span&gt;(&lt;span class="pl-s"&gt;"ignored.txt"&lt;/span&gt;)
        &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;open&lt;/span&gt;(&lt;span class="pl-s"&gt;"test_dir/ignored.txt"&lt;/span&gt;, &lt;span class="pl-s"&gt;"w"&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;f&lt;/span&gt;.&lt;span class="pl-en"&gt;write&lt;/span&gt;(&lt;span class="pl-s"&gt;"This file should be ignored"&lt;/span&gt;)
        &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-en"&gt;open&lt;/span&gt;(&lt;span class="pl-s"&gt;"test_dir/included.txt"&lt;/span&gt;, &lt;span class="pl-s"&gt;"w"&lt;/span&gt;) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;f&lt;/span&gt;:
            &lt;span class="pl-s1"&gt;f&lt;/span&gt;.&lt;span class="pl-en"&gt;write&lt;/span&gt;(&lt;span class="pl-s"&gt;"This file should be included"&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-s1"&gt;runner&lt;/span&gt;.&lt;span class="pl-en"&gt;invoke&lt;/span&gt;(&lt;span class="pl-s1"&gt;cli&lt;/span&gt;, [&lt;span class="pl-s"&gt;"test_dir"&lt;/span&gt;])
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;exit_code&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"test_dir/ignored.txt"&lt;/span&gt; &lt;span class="pl-c1"&gt;not&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"test_dir/included.txt"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&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-s1"&gt;runner&lt;/span&gt;.&lt;span class="pl-en"&gt;invoke&lt;/span&gt;(&lt;span class="pl-s1"&gt;cli&lt;/span&gt;, [&lt;span class="pl-s"&gt;"test_dir"&lt;/span&gt;, &lt;span class="pl-s"&gt;"--ignore-gitignore"&lt;/span&gt;])
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;exit_code&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"test_dir/ignored.txt"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"This file should be ignored"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&lt;/span&gt;
        &lt;span class="pl-k"&gt;assert&lt;/span&gt; &lt;span class="pl-s"&gt;"test_dir/included.txt"&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;result&lt;/span&gt;.&lt;span class="pl-s1"&gt;output&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;Then I ran them... and they failed. Here's the error message:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    def test_ignore_gitignore(tmpdir):
        runner = CliRunner()
        with tmpdir.as_cwd():
            os.makedirs("test_dir")
            with open("test_dir/.gitignore", "w") as f:
                f.write("ignored.txt")
            with open("test_dir/ignored.txt", "w") as f:
                f.write("This file should be ignored")
            with open("test_dir/included.txt", "w") as f:
                f.write("This file should be included")
    
            result = runner.invoke(cli, ["test_dir"])
            assert result.exit_code == 0
&amp;gt;           assert "test_dir/ignored.txt" not in result.output
E           AssertionError: assert 'test_dir/ignored.txt' not in 'test_dir/in...red\n\n---\n'
E             
E             'test_dir/ignored.txt' is contained here:
E               uded
E               
E               ---
E               test_dir/ignored.txt
E               ---...
E             
E             ...Full output truncated (3 lines hidden), use '-vv' to show
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I saved that message to `error.txt and ran this command to send it back to Claude 3 Opus:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;cat error.txt &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -c&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;llm -c&lt;/code&gt; command stands for "continue" - it will take any input or prompt and pass that back to the model along with the previous logged prompts and responses, equivalent to asking a follow-up question in an interface like ChatGPT.&lt;/p&gt;
&lt;p&gt;Claude gave me back a corrected version of the &lt;code&gt;should_ignore()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;should_ignore&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;, &lt;span class="pl-s1"&gt;gitignore_rules&lt;/span&gt;):
    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;rule&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;gitignore_rules&lt;/span&gt;:
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-en"&gt;fnmatch&lt;/span&gt;(&lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;.&lt;span class="pl-en"&gt;basename&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;), &lt;span class="pl-s1"&gt;rule&lt;/span&gt;):
            &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-c1"&gt;True&lt;/span&gt;
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;.&lt;span class="pl-en"&gt;isdir&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;) &lt;span class="pl-c1"&gt;and&lt;/span&gt; &lt;span class="pl-en"&gt;fnmatch&lt;/span&gt;(&lt;span class="pl-s1"&gt;os&lt;/span&gt;.&lt;span class="pl-s1"&gt;path&lt;/span&gt;.&lt;span class="pl-en"&gt;basename&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&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-s1"&gt;rule&lt;/span&gt;):
            &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-c1"&gt;True&lt;/span&gt;
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-c1"&gt;False&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;Sending follow-on prompts with &lt;code&gt;llm -c&lt;/code&gt; seemed to be working well, so I ran this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm -c &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Fill out the Usage section of the README for this project - just return that section as markdown&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And it gave me back a pretty decent README:&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;Usage&lt;/h2&gt;
&lt;p&gt;To use &lt;code&gt;files-to-prompt&lt;/code&gt;, simply provide the path to the directory you want to process:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt path/to/directory&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will output the contents of every file in the directory and its subdirectories, with each file preceded by its relative path and separated by &lt;code&gt;---&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Options&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--include-hidden&lt;/code&gt;: Include files and folders starting with &lt;code&gt;.&lt;/code&gt; (hidden files and directories).&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt path/to/directory --include-hidden&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--ignore-gitignore&lt;/code&gt;: Ignore &lt;code&gt;.gitignore&lt;/code&gt; files and include all files.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt path/to/directory --ignore-gitignore&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Example&lt;/h3&gt;
&lt;p&gt;Suppose you have a directory structure like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_directory/
├── file1.txt
├── file2.txt
├── .hidden_file.txt
└── subdirectory/
    └── file3.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Running &lt;code&gt;files-to-prompt my_directory&lt;/code&gt; will output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_directory/file1.txt
---
Contents of file1.txt
---
my_directory/file2.txt
---
Contents of file2.txt
---
my_directory/subdirectory/file3.txt
---
Contents of file3.txt
---
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you run &lt;code&gt;files-to-prompt my_directory --include-hidden&lt;/code&gt;, the output will also include &lt;code&gt;.hidden_file.txt&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;my_directory/.hidden_file.txt
---
Contents of .hidden_file.txt
---
...
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;I particularly liked the directory structure diagram.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/5b7ce53bf85ba1403af81e3be83ca32f"&gt;the full transcript including my follow-ups&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I committed and pushed everything I had so far &lt;a href="https://github.com/simonw/files-to-prompt/commits/main/"&gt;to GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After one last review of the README I noticed it had used the phrase "simply provide the path to the directory". I don't like using words like &lt;em&gt;simply&lt;/em&gt; in documentation, so &lt;a href="https://github.com/simonw/files-to-prompt/commit/c9c60d57a3a92aebe8112c6fdd6158093982ab9f"&gt;I fixed that&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And I shipped &lt;a href="https://github.com/simonw/files-to-prompt/releases/tag/0.1"&gt;version 0.1&lt;/a&gt; of the software! Almost every line of code, tests and documentation written by Claude 3 Opus.&lt;/p&gt;
&lt;h4 id="iterating-on-the-project"&gt;Iterating on the project&lt;/h4&gt;
&lt;p&gt;I've added several features since that initial implementation, almost all of which were primarily written by prompting Claude 3 Opus.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/files-to-prompt/issues/2"&gt;Issue #2: Take multiple arguments for files and directories to include&lt;/a&gt; changed the tool such that &lt;code&gt;files-to-prompt README.md tests/&lt;/code&gt; would include both the &lt;code&gt;README.md&lt;/code&gt; file and all files in the &lt;code&gt;tests/&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;The sequence of prompts to get there was as follows:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;cat files_to_prompt/cli.py &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m opus --system &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;Modify this file. It should take multiple arguments in a variable called paths.&lt;/span&gt;
&lt;span class="pl-s"&gt;Each of those argumets might be a path to a file or it might be a path to a&lt;/span&gt;
&lt;span class="pl-s"&gt;directory - if any of the arguments do not correspoind to a file or directory&lt;/span&gt;
&lt;span class="pl-s"&gt;it should raise a click error.&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;It should then do what it does already but for all files &lt;/span&gt;
&lt;span class="pl-s"&gt;files-recursively-contained-within-folders that are passed to it.&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;It should still obey the gitignore logic.&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then these to update the tests:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt files_to_prompt tests &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m opus --system &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;rewrite the tests to cover the ability to pass multiple files and&lt;/span&gt;
&lt;span class="pl-s"&gt;folders to the tool&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;

files-to-prompt files_to_prompt tests &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m opus --system &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;add one last test which tests .gitignore and include_hidden against&lt;/span&gt;
&lt;span class="pl-s"&gt;an example that mixes single files and directories of files together&lt;/span&gt;
&lt;span class="pl-s"&gt;in one invocation&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I didn't like the filenames it was using in that last test, so I used &lt;a href="https://github.com/simonw/symbex"&gt;symbex&lt;/a&gt; to extract just the implementation of that test and told it to rewrite it:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;symbex test_mixed_paths_with_options &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m opus --system &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;rewrite this test so the filenames are more obvious, thinks like&lt;/span&gt;
&lt;span class="pl-s"&gt;ignored_in_gitignore.txt&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And this to add one last test that combined all of the options:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm -c &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;add a last bit to that test for&lt;/span&gt;
&lt;span class="pl-s"&gt;["test_dir", "single_file.txt", "--ignore-gitignore", "--include-hidden"]&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/files-to-prompt/issues/2"&gt;The issue&lt;/a&gt; includes links to the full transcripts for the above.&lt;/p&gt;
&lt;h4 id="updating-a-diff"&gt;Updating a diff from a pull request&lt;/h4&gt;
&lt;p&gt;I quietly released &lt;code&gt;files-to-prompt&lt;/code&gt; two weeks ago. &lt;a href="https://github.com/simonw/files-to-prompt/pull/4"&gt;Dipam Vasani&lt;/a&gt; had spotted it and &lt;a href="https://github.com/simonw/files-to-prompt/pull/4"&gt;opened a pull request&lt;/a&gt; adding the ability to ignore specific files, by passing &lt;code&gt;--ignore-patterns '*.md'&lt;/code&gt; as an option.&lt;/p&gt;
&lt;p&gt;The problem was... I'd landed some of my own changes before I got around to reviewing his PR - so it would no longer cleanly apply.&lt;/p&gt;
&lt;p&gt;It turns out I could resolve that problem using Claude 3 Opus as well, by asking it to figure out the change from Dipam's diff.&lt;/p&gt;
&lt;p&gt;I pulled a copy of his PR as a diff like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;wget &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;https://github.com/simonw/files-to-prompt/pull/4.diff&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then I fed both the diff and the relevant files from the project into Claude:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt 4.diff files_to_prompt/cli.py tests/test_files_to_prompt.py &lt;span class="pl-k"&gt;|&lt;/span&gt; \
  llm -m opus --system \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Apply the change described in the diff to the project - return updated cli.py and tests&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It didn't quite work - it reverted one of my earlier changes. So I prompted:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;llm -c &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;you undid the change where it could handle multiple paths -&lt;/span&gt;
&lt;span class="pl-s"&gt;I want to keep that, I only want to add the new --ignore-patterns option&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And that time it worked! &lt;a href="https://gist.github.com/simonw/8b8394e320c895c792736e7e85c40c23"&gt;Transcript here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I merged Claude's work into the existing PR to ensure Dipam got credit for his work, then &lt;a href="https://github.com/simonw/files-to-prompt/commit/f8af0fad7f206f029869cda7b4a1846b19aee423"&gt;landed it&lt;/a&gt; and pushed it out in a release.&lt;/p&gt;
&lt;h4 id="was-this-worthwhile"&gt;Was this worthwhile?&lt;/h4&gt;
&lt;p&gt;As an exercise in testing the limits of what's possible with command-line LLM access and the current most powerful available LLM, this was absolutely worthwhile. I got working software with comprehensive tests and documentation, and had a lot of fun experimenting with prompts along the way.&lt;/p&gt;
&lt;p&gt;It's worth noting that this project was &lt;em&gt;incredibly&lt;/em&gt; low stakes. &lt;code&gt;files-to-prompt&lt;/code&gt; is a tiny tool that does something very simple. Any bugs or design flaws really don't matter. It's perfect for trying out this alternative approach to development.&lt;/p&gt;
&lt;p&gt;I also got the software built a whole lot faster than if I'd written it myself, and with features like &lt;code&gt;.gitignore&lt;/code&gt; support (albeit rudimentary) that I may not have bothered with working alone. That's a good example of a feature that's just fiddly enough that I might decide not to invest the time needed to get it to work.&lt;/p&gt;
&lt;p&gt;Is this the best possible version of this software? Definitely not. But with comprehensive documentation and automated tests it's high enough quality that I'm not ashamed to release it with my name on it.&lt;/p&gt;
&lt;p&gt;A year ago I might have felt &lt;em&gt;guilty&lt;/em&gt; about using LLMs to write code for me in this way. I'm over that now: I'm still doing the work, but I now have a powerful tool that can help accelerate the process.&lt;/p&gt;
&lt;h4 id="real-work"&gt;Using this pattern for real work&lt;/h4&gt;
&lt;p&gt;I've since used the same pattern for some smaller modifications to some of my more significant projects. This morning I used it to upgrade my &lt;a href="https://github.com/simonw/datasette-cors"&gt;datasette-cors&lt;/a&gt; plugin to add support for new features I had added to the underlying &lt;a href="https://github.com/simonw/asgi-cors"&gt;asgi-cors&lt;/a&gt; library. Here's the prompt sequence I used:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;files-to-prompt ../asgi-cors/asgi_cors.py datasette_cors.py &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m opus -s \
&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Output a new datasette_cors.py plugin that adds headers and methods and max_age config options&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;

files-to-prompt test_datasette_cors.py &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -c \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Update these tests to exercise the new options as well&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;

cat README.md &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -c \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Update the README to document the new config options&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And the &lt;a href="https://gist.github.com/simonw/5e379cc3ed610caf8dfbdfe25b19874a"&gt;full transcript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I reviewed this code &lt;em&gt;very carefully&lt;/em&gt; &lt;a href="https://github.com/simonw/datasette-cors/commit/53b126b9f9ae52c9f8f2ec8cf751b7f1c9b804de"&gt;before landing it&lt;/a&gt;. It's absolutely what I would have written myself without assistance from Claude.&lt;/p&gt;
&lt;p&gt;Time elapsed for this change? The first prompt was logged at 16:42:11 and the last at 16:44:24, so just over two minutes followed by a couple more minutes for the review. The &lt;a href="https://github.com/simonw/datasette-cors/issues/5"&gt;associated issue&lt;/a&gt; was open for five minutes total.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &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/llm"&gt;llm&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/files-to-prompt"&gt;files-to-prompt&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="cli"/><category term="projects"/><category term="ai"/><category term="prompt-engineering"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/><category term="anthropic"/><category term="claude"/><category term="files-to-prompt"/></entry></feed>