<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Eli Bendersky's website - Software &amp; Tools</title><link href="https://eli.thegreenplace.net/" rel="alternate"></link><link href="https://eli.thegreenplace.net/feeds/software-tools.atom.xml" rel="self"></link><id>https://eli.thegreenplace.net/</id><updated>2025-10-25T13:23:02-07:00</updated><entry><title>LaTeX, LLMs and Boring Technology</title><link href="https://eli.thegreenplace.net/2025/latex-llms-and-boring-technology/" rel="alternate"></link><published>2025-10-25T06:20:00-07:00</published><updated>2025-10-25T13:23:02-07:00</updated><author><name>Eli Bendersky</name></author><id>tag:eli.thegreenplace.net,2025-10-25:/2025/latex-llms-and-boring-technology/</id><summary type="html">&lt;p&gt;Depending on your particular use case, &lt;a class="reference external" href="https://boringtechnology.club/"&gt;choosing boring technology&lt;/a&gt;
is often a good idea. Recently, I've been thinking more and more about how
the rise and increase in power of LLMs affects this choice.&lt;/p&gt;
&lt;p&gt;By definition, boring technology has been around for a long time. Piles of
content have been …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Depending on your particular use case, &lt;a class="reference external" href="https://boringtechnology.club/"&gt;choosing boring technology&lt;/a&gt;
is often a good idea. Recently, I've been thinking more and more about how
the rise and increase in power of LLMs affects this choice.&lt;/p&gt;
&lt;p&gt;By definition, boring technology has been around for a long time. Piles of
content have been written and produced about it: tutorials, books, videos,
reference manuals, examples, blog posts and so on. All of this is consumed
during the LLM training process, making LLMs better and better at reasoning
about such technology.&lt;/p&gt;
&lt;p&gt;Conversely, &amp;quot;shiny technology&amp;quot; is new, and has much less material available. As
a result, LLMs won't be as familiar with it. This applies to many domains, but
one specific example for me personally is in the context of LaTeX.&lt;/p&gt;
&lt;p&gt;LaTeX certainly fits the &amp;quot;boring technology&amp;quot; bill. It's decades old, and has
been the mainstay of academic writing since the 1980s. When I used it for the
first time in 2002 (for a project report in my university AI class), it was
already very old. But people keep working on it and fixing issues; it's easy to
install and its wealth of capabilities and community size are staggering.
Moreover, people keep working &lt;em&gt;with&lt;/em&gt; it, producing more and more content and
examples the LLMs can ingest and learn from.&lt;/p&gt;
&lt;p&gt;I keep hearing about the advantages of new and shiny systems like
Typst. However, with the help of LLMs, almost none of the advantages seem
meaningful to me. LLMs are &lt;em&gt;great&lt;/em&gt; at LaTeX and help a lot with learning or
remembering the syntax, finding the right packages, deciphering errors and
even generating tedious parts like tables and charts, significantly reducing
the need for scripting &lt;a class="footnote-reference" href="#footnote-1" id="footnote-reference-1"&gt;[1]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can use LLMs either as standalone or fully integrated into your LaTeX
environment; Overleaf has a built-in AI helper, and for local editing you can
use VSCode plugins or other tools. I'm personally &lt;a class="reference external" href="https://eli.thegreenplace.net/2025/notes-on-using-latex-to-generate-formulae/"&gt;content with TeXstudio&lt;/a&gt;
and use LLMs as standalone help, but YMMV.&lt;/p&gt;
&lt;p&gt;There are many examples where boring technology and LLMs go well together. The
main criticism of boring technology is typically that it's &amp;quot;too big, full of
cruft, difficult to understand&amp;quot;. LLMs really help cutting through the learning
curve though, and all that &amp;quot;cruft&amp;quot; is very likely to become useful some time in
the future when you graduate from the basic use cases.&lt;/p&gt;
&lt;p&gt;To be clear: Typst looks really cool, and kudos to the team behind it! All I'm
saying in this post is that for me - personaly - the choice for now is to stick
with LaTeX as a &amp;quot;boring technology&amp;quot;.&lt;/p&gt;
&lt;div class="section" id="appendix-some-examples-of-llms-being-helpful-with-latex"&gt;
&lt;h2&gt;Appendix: some examples of LLMs being helpful with LaTeX&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p class="first"&gt;For finding the right math symbols, I rarely need to scan reference materials
any longer. LLMs will easily answer questions like &amp;quot;what's that squiggly
Greek letter used in math, and its latex symbol?&amp;quot; or &amp;quot;write the latex for
Green's theorem, integral form&amp;quot;.
For the trickiest / largest equations, LLMs are very good at &amp;quot;here's a picture
I took of my equation, give me its latex code&amp;quot; these days &lt;a class="footnote-reference" href="#footnote-2" id="footnote-reference-2"&gt;[2]&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;&amp;quot;Here's a piece of code and the LaTeX error I'm getting on it; what's wrong?&amp;quot;&lt;/p&gt;
&lt;p&gt;This is made more ergonomic by editor integrations, but I personally find that
LaTeX's error message problem is hugely overblown. 95% of the errors are
reasonably clear, and serious sleuthing is only rarely required in practice.
In that minority of cases, pasting some code and the error into a standalone
LLM isn't a serious time drain.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Generating TikZ diagrams and plots. For this, the hardest part is getting
started and finding the right element names, and so on. It's very useful to
just ask an LLM to emit something initial and then tweak it manually later,
as needed. You can also ask the LLM to explain each thing it emits in
detail - this is a great learning tool for deeper understanding.&lt;/p&gt;
&lt;p&gt;Recently I had luck going &amp;quot;meta&amp;quot; with this: when the diagram has repetitive
elements, I may ask the LLM to &amp;quot;write a Python program that generates a TikZ
diagram ...&amp;quot;, and it works well.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Generating and populating tables, and converting them from other data
formats or screenshots.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p class="first"&gt;Help with formatting and typesetting (how do I change margins to XXX and
spacing to YYY).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr class="docutils" /&gt;
&lt;table class="docutils footnote" frame="void" id="footnote-1" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label" /&gt;&lt;col /&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="#footnote-reference-1"&gt;[1]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;p class="first"&gt;When it comes to scripting, I generally prefer sticking to real
programming languages anyway. If there's anything non-trivial to
auto-generate I wouldn't use a LaTeX macro, but would write a Python
program to generate whatever I need and embed it into the document
with something like &lt;tt class="docutils literal"&gt;\input{}&lt;/tt&gt;.&lt;/p&gt;
&lt;p class="last"&gt;Typst's scripting system may be marketed as &amp;quot;clean and powerful&amp;quot;, but
why learn yet another scripting language?&lt;/p&gt;
&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="footnote-2" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label" /&gt;&lt;col /&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="#footnote-reference-2"&gt;[2]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;p class="first"&gt;Ignoring LaTeX's equation notation and doing their own thing is one of
the biggest mistakes Typst makes, in my opinion. LaTeX's notation may
not be perfect, but it's near universal at this point with support in
almost all math-aware tools.&lt;/p&gt;
&lt;p class="last"&gt;Typst's math mode is a clear sign of the second system effect, and isn't
&lt;a class="reference external" href="https://laurmaedje.github.io/posts/math-mode-problem/"&gt;even stable&lt;/a&gt;.&lt;/p&gt;
&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
</content><category term="misc"></category><category term="Math"></category><category term="Blogging"></category><category term="Software &amp; Tools"></category></entry><entry><title>Notes on using LaTeX to generate formulae</title><link href="https://eli.thegreenplace.net/2025/notes-on-using-latex-to-generate-formulae/" rel="alternate"></link><published>2025-10-11T08:13:00-07:00</published><updated>2025-10-11T15:15:50-07:00</updated><author><name>Eli Bendersky</name></author><id>tag:eli.thegreenplace.net,2025-10-11:/2025/notes-on-using-latex-to-generate-formulae/</id><summary type="html">&lt;p&gt;This post collects some notes on using LaTeX to render mathematical documents
and formulae, mostly focused on a Linux machine. For background, I typically
use LaTeX for one of two (related) purposes:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Render math for my blog posts, which are usually written
&lt;a class="reference external" href="https://eli.thegreenplace.net/2014/blogging-setup-with-pelican/"&gt;using reStructuredText&lt;/a&gt;.
This sometimes includes diagrams generated using …&lt;/li&gt;&lt;/ol&gt;</summary><content type="html">&lt;p&gt;This post collects some notes on using LaTeX to render mathematical documents
and formulae, mostly focused on a Linux machine. For background, I typically
use LaTeX for one of two (related) purposes:&lt;/p&gt;
&lt;ol class="arabic simple"&gt;
&lt;li&gt;Render math for my blog posts, which are usually written
&lt;a class="reference external" href="https://eli.thegreenplace.net/2014/blogging-setup-with-pelican/"&gt;using reStructuredText&lt;/a&gt;.
This sometimes includes diagrams generated using TikZ.&lt;/li&gt;
&lt;li&gt;Write personal (unpublished) notes on math-y subjects entirely in LaTeX.
These are typically short (up to 10-20 pages), single-subject booklets.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I don't currently use LaTeX for either precise typesetting or for
authoring very large, book-sized documents.&lt;/p&gt;
&lt;div class="section" id="editing"&gt;
&lt;h2&gt;Editing&lt;/h2&gt;
&lt;p&gt;For day-to-day authoring, I find &lt;a class="reference external" href="https://www.texstudio.org/"&gt;TeXstudio&lt;/a&gt; to be
excellent. It has everything I need for local editing with a convenient preview
window. I really like that TeXstudio doesn't hide the fact that it's just a
graphical veneer on top of command-line LaTeX tooling, and lets you examine what
it's doing through logs.&lt;/p&gt;
&lt;p&gt;Note that web-based solutions like Overleaf exist; I can see myself using that,
especially if collaborating with others or having to author LaTeX from a
diverse set of computers and OSes, but for local editing of git-backed text
files, TeXstudio is great.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="converting-to-other-formats-with-pandoc"&gt;
&lt;h2&gt;Converting to other formats with pandoc&lt;/h2&gt;
&lt;p&gt;&lt;a class="reference external" href="https://pandoc.org/"&gt;pandoc&lt;/a&gt; is very capable for converting documents from
LaTeX to other formats. Recently I find that it's easier to write math-heavy
blog posts in LaTeX, and then convert them to reStructuredText with &lt;tt class="docutils literal"&gt;pandoc&lt;/tt&gt;.&lt;/p&gt;
&lt;p&gt;For example, the recent &lt;a class="reference external" href="https://eli.thegreenplace.net/2025/hilbert-space-treating-functions-as-vectors/"&gt;post on Hilbert spaces&lt;/a&gt;
was written like this and then converted using this command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ pandoc -f latex -s -t rst hilbert.tex
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The resulting reStructuredText is very readable and requires very little
tweaking before final publishing. &lt;tt class="docutils literal"&gt;pandoc&lt;/tt&gt; supports many formats, so if you
use Markdown or something else, it should work similarly well.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="rendering-standalone-formulae-or-diagrams"&gt;
&lt;h2&gt;Rendering standalone formulae or diagrams&lt;/h2&gt;
&lt;p&gt;A useful feature of LaTeX tooling is the ability to render a specific formula
in standalone mode to an image. We can write the formula into its own file
(call it &lt;tt class="docutils literal"&gt;standaloneformula.tex&lt;/tt&gt;):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;\documentclass&lt;/span&gt;&lt;span class="na"&gt;[preview]&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;standalone&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;\usepackage&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;amsmath&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;\begin&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;document&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;span class="s"&gt;\(&lt;/span&gt;&lt;span class="nb"&gt;&lt;/span&gt;
&lt;span class="nv"&gt;\int&lt;/span&gt;&lt;span class="nb"&gt;_{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;\infty&lt;/span&gt;&lt;span class="nb"&gt;}^&lt;/span&gt;&lt;span class="nv"&gt;\infty&lt;/span&gt;&lt;span class="nb"&gt; e^{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;x^&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="nb"&gt;}&lt;/span&gt;&lt;span class="nv"&gt;\,&lt;/span&gt;&lt;span class="nb"&gt;dx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;\sqrt&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;\pi&lt;/span&gt;&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;span class="s"&gt;\)&lt;/span&gt;
&lt;span class="k"&gt;\end&lt;/span&gt;&lt;span class="nb"&gt;{&lt;/span&gt;document&lt;span class="nb"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In case you were wondering, this is the Gaussian integral:&lt;/p&gt;
&lt;object class="align-center" data="https://eli.thegreenplace.net/images/math/1a96fcbd751829566b9da856edd88c2916739440.svg" style="height: 41px;" type="image/svg+xml"&gt;\[\int_{-\infty}^\infty e^{-x^2}\,dx=\sqrt{\pi}\]&lt;/object&gt;
&lt;p&gt;Once we have that standalone &lt;tt class="docutils literal"&gt;.tex&lt;/tt&gt; file, there's a number of things we can
do. First, the &lt;a class="reference external" href="https://www.tug.org/texlive/"&gt;texlive&lt;/a&gt; package should be
installed &lt;a class="footnote-reference" href="#footnote-1" id="footnote-reference-1"&gt;[1]&lt;/a&gt;. Using &lt;tt class="docutils literal"&gt;apt&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ sudo apt install texlive-full
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, we can run the tools from &lt;tt class="docutils literal"&gt;texlive&lt;/tt&gt;, for example &lt;tt class="docutils literal"&gt;pdflatex&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ pdflatex standaloneformula.tex
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This creates a PDF file that's useful for previews. To convert the &lt;tt class="docutils literal"&gt;.tex&lt;/tt&gt; file
to an image in SVG format, we'll use a two-step process:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ latex standaloneformula.tex

... generates standaloneformula.dvi

$ dvisvgm standaloneformula.dvi

... generates standaloneformula.svg
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If we want a PNG file instead of SVG:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ dvipng -D 300 standaloneformula.dvi
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;tt class="docutils literal"&gt;latexmk&lt;/tt&gt; tool can build a &lt;tt class="docutils literal"&gt;.tex&lt;/tt&gt; file into a PDF whenever the input
file changes, so running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ latexmk -pvc standaloneformula.tex
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And opening the PDF in a separate window, we can observe live refreshes of
edits without having to recompile explicitly. While useful in some scenarios,
I find that TeXstudio already does this well.&lt;/p&gt;
&lt;p&gt;The same tooling flow works for TikZ diagrams! A standalone LaTeX document
containing a single &lt;tt class="docutils literal"&gt;tikzpicture&lt;/tt&gt; element can also be rendered to a SVG or
PNG using the same exact commands.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="docker-versions"&gt;
&lt;h2&gt;Docker versions&lt;/h2&gt;
&lt;p&gt;If you'd rather not install all these tools directly but use Docker instead,
the &lt;tt class="docutils literal"&gt;texlive&lt;/tt&gt; image can be used to do the same things:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ docker pull texlive/texlive:latest
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And now we can use the same invocations, just through docker:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ docker run --rm -u $(id -u):$(id -g) \
    -v &amp;quot;$PWD&amp;quot;:/workdir -w /workdir texlive/texlive:latest \
    latex standaloneformula.tex

$ docker run --rm -u $(id -u):$(id -g) \
    -v &amp;quot;$PWD&amp;quot;:/workdir -w /workdir texlive/texlive:latest \
    dvisvgm standaloneformula.dvi
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="positioning-of-formulae-embedded-in-text"&gt;
&lt;h2&gt;Positioning of formulae embedded in text&lt;/h2&gt;
&lt;p&gt;When a formula like &lt;object class="valign-m7" data="https://eli.thegreenplace.net/images/math/58d3c40cf29ef7230b9016e5b256cd8d9cdfb9c8.svg" style="height: 23px;" type="image/svg+xml"&gt;\frac{n+1}{n^2-1}&lt;/object&gt; is embedded in text, it should
be aligned properly to look good with the surrounding text. The information
required to do this is emitted by tools like &lt;tt class="docutils literal"&gt;dvisvgm&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;dvipng&lt;/tt&gt;; for
example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ dvisvgm standaloneformula.dvi
pre-processing DVI file (format version 2)
processing page 1
  computing extents based on data set by preview package (version 14.0.6)
  width=81.267395pt, height=9.86894pt, depth=4.388947pt
  graphic size: 81.267395pt x 14.257887pt (28.562223mm x 5.011074mm)
  output written to standaloneformula.svg
1 of 1 page converted in 0.147623 seconds
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;height=...,&lt;/span&gt; &lt;span class="pre"&gt;depth=...&lt;/span&gt;&lt;/tt&gt; line in the output. The height is the total
height of the formula, and depth is its height below the &amp;quot;baseline&amp;quot; (how much
down it should stick out from the line). In my blog, these two are translated
to attributes on the image element embedding the SVG. Height is translated to
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;style=&amp;quot;height:&lt;/span&gt; ...&lt;/tt&gt; and depth to &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;vertical-align:&lt;/span&gt; ...&lt;/tt&gt;.&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;table class="docutils footnote" frame="void" id="footnote-1" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label" /&gt;&lt;col /&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="#footnote-reference-1"&gt;[1]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;If the machine already has TeXstudio installed, &lt;tt class="docutils literal"&gt;texlive&lt;/tt&gt; is almost
certainly installed as well.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
</content><category term="misc"></category><category term="Math"></category><category term="Blogging"></category><category term="Software &amp; Tools"></category></entry><entry><title>gemini-cli: Access Gemini models from the command-line</title><link href="https://eli.thegreenplace.net/2024/gemini-cli-access-gemini-models-from-the-command-line/" rel="alternate"></link><published>2024-02-21T06:04:00-08:00</published><updated>2024-02-21T14:06:19-08:00</updated><author><name>Eli Bendersky</name></author><id>tag:eli.thegreenplace.net,2024-02-21:/2024/gemini-cli-access-gemini-models-from-the-command-line/</id><summary type="html">&lt;p&gt;This post is about a new command-line tool I've recently built in Go -
&lt;a class="reference external" href="https://github.com/eliben/gemini-cli"&gt;gemini-cli&lt;/a&gt;, and how to use it for
LLM-based data analysis with Google's Gemini models.&lt;/p&gt;
&lt;p&gt;Background: I've been reading &lt;a class="reference external" href="https://simonwillison.net/"&gt;Simon Willison's&lt;/a&gt; posts about LLMs with interest, especially his
work on tools that leverage LLMs and SQLite to create …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This post is about a new command-line tool I've recently built in Go -
&lt;a class="reference external" href="https://github.com/eliben/gemini-cli"&gt;gemini-cli&lt;/a&gt;, and how to use it for
LLM-based data analysis with Google's Gemini models.&lt;/p&gt;
&lt;p&gt;Background: I've been reading &lt;a class="reference external" href="https://simonwillison.net/"&gt;Simon Willison's&lt;/a&gt; posts about LLMs with interest, especially his
work on tools that leverage LLMs and SQLite to create &lt;a class="reference external" href="https://til.simonwillison.net/llms/openai-embeddings-related-content"&gt;fun little
analysis pipelines for local documents&lt;/a&gt;.
Since I've recently done some Go work on &lt;a class="reference external" href="https://github.com/google/generative-ai-go"&gt;Google's Gemini SDKs&lt;/a&gt; (also in &lt;a class="reference external" href="https://github.com/tmc/langchaingo"&gt;langchaingo&lt;/a&gt;)
and wrote a &lt;a class="reference external" href="https://eli.thegreenplace.net/2023/using-gemini-models-from-go/"&gt;couple of&lt;/a&gt;
&lt;a class="reference external" href="https://eli.thegreenplace.net/2024/using-gemini-models-in-go-with-langchaingo/"&gt;blog posts&lt;/a&gt;
about it, I was interested in creating a similar pipeline for myself using
Go and Gemini models. This is how the idea for &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt;&lt;/tt&gt; was born.&lt;/p&gt;
&lt;div class="section" id="the-tool"&gt;
&lt;h2&gt;The tool&lt;/h2&gt;
&lt;p&gt;Like any Go command-line tool, &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt;&lt;/tt&gt; is very easy to install:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ go install github.com/eliben/gemini-cli@latest
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And you're good to go! It will want a Gemini API key set in the
&lt;tt class="docutils literal"&gt;GEMINI_API_KEY&lt;/tt&gt; env var or passed with the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;--key&lt;/span&gt;&lt;/tt&gt; flag. If you don't have
an API key yet, you can get one quickly and for free from &lt;a class="reference external" href="https://ai.google.dev/"&gt;https://ai.google.dev/&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="the-motivating-task"&gt;
&lt;h2&gt;The motivating task&lt;/h2&gt;
&lt;p&gt;For a while I've been interested in adding a &amp;quot;related posts&amp;quot; feature to my blog.
It was clear that I'll want to use &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Sentence_embedding"&gt;embeddings&lt;/a&gt; to convert my posts to
vector space and then use vector similarity to find related posts. Check
out my earlier &lt;a class="reference external" href="https://eli.thegreenplace.net/2023/retrieval-augmented-generation-in-go/"&gt;post on RAG&lt;/a&gt;
for additional information on these techniques.&lt;/p&gt;
&lt;p&gt;Before starting to write the code, however, I wanted to experiment with
a command-line tool so I could rapidly prototype. Think of it as crafting
some text processing pipeline from classical Unix command-line tools before
trying to implement it in a programming language. &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt;&lt;/tt&gt; excels for
precisely such prototyping.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="finding-related-posts"&gt;
&lt;h2&gt;Finding related posts&lt;/h2&gt;
&lt;p&gt;Let's see how to use &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt;&lt;/tt&gt; for my task. I have access to the contents
of my blog posts on the file system as a large bunch of
&lt;a class="reference external" href="https://en.wikipedia.org/wiki/ReStructuredText"&gt;reStructuredText&lt;/a&gt; and HTML
files. These are private, but you're free to replicate this experiment for
any collection of textual documents you have handy. It will even work on
programming language source code!&lt;/p&gt;
&lt;p&gt;Let's first get the lay of the land - how many files are there &lt;a class="footnote-reference" href="#footnote-1" id="footnote-reference-1"&gt;[1]&lt;/a&gt;?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ pss -f --rst content/|wc -l
279
$ pss -f --html content/|wc -l
1064
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;OK, so a bit over 1300 overall. Let's start by computing the embeddings for
the reST files. We'll ask &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt;&lt;/tt&gt; to write it into a new SQLite
DB called &lt;tt class="docutils literal"&gt;blogemb.db&lt;/tt&gt;, using its &lt;tt class="docutils literal"&gt;embed db&lt;/tt&gt; subcommand:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ export GEMINI_API_KEY=...
$ gemini-cli embed db blogemb.db --files content/,&amp;quot;*.rst&amp;quot;
Found 279 values to embed
Splitting to 9 batches
Embedding batch #1 / 9, size=32
Embedding batch #2 / 9, size=32
Embedding batch #3 / 9, size=32
Embedding batch #4 / 9, size=32
Embedding batch #5 / 9, size=32
Embedding batch #6 / 9, size=32
Embedding batch #7 / 9, size=32
Embedding batch #8 / 9, size=32
Embedding batch #9 / 9, size=23
Collected 279 embeddings; inserting into table embeddings
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let's look at the DB file using the &lt;tt class="docutils literal"&gt;sqlite3&lt;/tt&gt; command-line tool:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ sqlite3 blogemb.db
SQLite version 3.37.2 2022-01-06 13:25:41
Enter &amp;quot;.help&amp;quot; for usage hints.

sqlite&amp;gt; .tables
embeddings

sqlite&amp;gt; .schema
CREATE TABLE embeddings (
id TEXT PRIMARY KEY,
embedding BLOB
);

sqlite&amp;gt; select count(*) from embeddings;
279

sqlite&amp;gt; select id, length(embedding) from embeddings limit 10;
content/2014/blogging-setup-with-pelican.rst|3072
content/2014/c++-perfect-forwarding-and-universal-references.rst|3072
content/2014/derivation-normal-equation-linear-regression.rst|3072
content/2014/goodbye-wordpress.rst|3072
content/2014/highlight-tab-gnome-terminal.rst|3072
content/2014/meshgrids-and-disambiguating-rows-and-columns-from-cartesian-coordinates.rst|3072
content/2014/samples-for-llvm-clang-library.rst|3072
content/2014/sfinae-and-enable-if.rst|3072
content/2014/summary-of-reading-july-september-2014.rst|3072
content/2014/summary-of-reading-october-december-2014.rst|3072
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As expected, we see 279 entries in the table; for each row the &lt;tt class="docutils literal"&gt;id&lt;/tt&gt; column
value is the path of the file and &lt;tt class="docutils literal"&gt;embedding&lt;/tt&gt; contains the embedding as a
blob. Embeddings are returned by the model as arrays of 32-bit floats, and
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt;&lt;/tt&gt; encodes them into a blob as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;// encodeEmbedding encodes an embedding into a byte buffer, e.g. for DB&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="c1"&gt;// storage as a blob.&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;encodeEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;emb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LittleEndian&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nb"&gt;panic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each &lt;tt class="docutils literal"&gt;float32&lt;/tt&gt; thus occupies 4 bytes; since our DB blobs are 3072 bytes long,
we can infer that each embedding vector has 768 elements; the embedding model
projects our text into 768-dimensional space &lt;a class="footnote-reference" href="#footnote-2" id="footnote-reference-2"&gt;[2]&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Back to our task, though. Note that &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt;&lt;/tt&gt; uses the batch-embedding
API of Gemini under the hood, so it's efficient for large input corpora. We
can control the batch size with a flag; just for fun, let's do this when
embedding the HTML files since there are so many of them:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ gemini-cli embed db blogemb.db --batch-size=64 --files content/,&amp;quot;*.html&amp;quot;
Found 1064 values to embed
Splitting to 17 batches
Embedding batch #1 / 17, size=64
Embedding batch #2 / 17, size=64
Embedding batch #3 / 17, size=64
Embedding batch #4 / 17, size=64
Embedding batch #5 / 17, size=64
Embedding batch #6 / 17, size=64
Embedding batch #7 / 17, size=64
Embedding batch #8 / 17, size=64
Embedding batch #9 / 17, size=64
Embedding batch #10 / 17, size=64
Embedding batch #11 / 17, size=64
Embedding batch #12 / 17, size=64
Embedding batch #13 / 17, size=64
Embedding batch #14 / 17, size=64
Embedding batch #15 / 17, size=64
Embedding batch #16 / 17, size=64
Embedding batch #17 / 17, size=40
Collected 1064 embeddings; inserting into table embeddings
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A brief note on performance: with a batch size of 64, this process took only
17 seconds - not bad for over a thousand documents. In the future I plan to
improve this time further with more concurrency and smarter batch size selection
&lt;a class="footnote-reference" href="#footnote-3" id="footnote-reference-3"&gt;[3]&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let's examine the resulting SQLite DB with all the embeddings:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ stat -c %s blogemb.db
5627904
$ echo &amp;quot;select count(*) from embeddings&amp;quot; | sqlite3 blogemb.db
1343
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;All 1343 entries have made it into the &lt;tt class="docutils literal"&gt;embeddings&lt;/tt&gt; table, and the total
size of the DB is just over 5 MiB.&lt;/p&gt;
&lt;p&gt;Now we're ready to look for related posts. The &lt;tt class="docutils literal"&gt;embed similar&lt;/tt&gt; subcommand
takes the name of a SQLite DB that holds all embeddings (like the one we've just
created) and a string of content to compare; it also accepts &lt;tt class="docutils literal"&gt;-&lt;/tt&gt; as an
indication that the input content will be piped through standard input, so
let's use that:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ gemini-cli embed similar blogemb.db - &amp;lt; content/2023/better-http-server-routing-in-go-122.rst
{&amp;quot;id&amp;quot;:&amp;quot;content/2023/better-http-server-routing-in-go-122.rst&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;1.0000001&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2021/rest-servers-in-go-part-2-using-a-router-package.rst&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.8904768&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2021/life-of-an-http-request-in-a-go-server.rst&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.83037585&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2021/rest-servers-in-go-part-5-middleware.rst&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.8136583&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2022/serving-static-files-and-web-apps-in-go.rst&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.7732284&amp;quot;}
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The output is in &lt;a class="reference external" href="https://jsonlines.org/examples/"&gt;the JSON Lines format&lt;/a&gt;,
and by default prints the ID and the similarity score (using cosine
similarity), sorted by decreasing similarity. Unsurprisingly, the most similar
post is... itself, with a perfect similarity score of 1.0&lt;/p&gt;
&lt;p&gt;The results look pretty good! The most similar posts found indeed are very
relevant to the one we were asking about. For fun, let's try a book review and
now with a larger list of output candidates (by using the &lt;tt class="docutils literal"&gt;topk&lt;/tt&gt; flag):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ gemini-cli embed similar blogemb.db --topk=10 - &amp;lt; content/2011/book-review-the-voyage-of-the-beagle-by-charles-darwin.html
{&amp;quot;id&amp;quot;:&amp;quot;content/2011/book-review-the-voyage-of-the-beagle-by-charles-darwin.html&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;1&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2008/book-review-the-origin-of-species-by-charles-darwin.html&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.80570847&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2006/book-review-the-selfish-gene-by-richard-dawkins.html&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.7845073&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2011/summary-of-reading-april-june-2011.html&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.7939675&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2004/book-review-a-short-history-of-nearly-by-bill-bryson.html&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.7784306&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2005/book-review-around-the-world-in-80-days-by-jules-verne.html&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.7792236&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2005/book-review-the-double-helix-by-james-watson.html&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.7658307&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2008/book-review-after-tamerlane-by-john-darwin.html&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.7641713&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2005/book-review-mysterious-island-by-jules-verne.html&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.7605505&amp;quot;}
{&amp;quot;id&amp;quot;:&amp;quot;content/2008/book-review-the-adventures-of-tom-sawyer-by-mark-twain.html&amp;quot;,&amp;quot;score&amp;quot;:&amp;quot;0.75610566&amp;quot;}
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="what-s-next"&gt;
&lt;h2&gt;What's next&lt;/h2&gt;
&lt;p&gt;For my task, I now have the basic information available to implement it, and
all the infrastructure for running experiments; with &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt;&lt;/tt&gt; in hand,
this took less than 5 minutes. All I needed to do is
&lt;a class="reference external" href="https://xkcd.com/1205/"&gt;write the tool&lt;/a&gt; :-)&lt;/p&gt;
&lt;p&gt;I really enjoyed building &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt;&lt;/tt&gt;; it's true to the spirit of simple,
textual Unix CLIs that can be easily combined together through pipes. Using
SQLite as the storage and retrieval format is also quite pleasant, and provides
interoperability for free.&lt;/p&gt;
&lt;p&gt;For you - if you're a Go developer interested in building stuff with LLMs and
getting started for free - I hope you find &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt;&lt;/tt&gt; useful. I've only
shown its &lt;tt class="docutils literal"&gt;embed *&lt;/tt&gt; subcommands, but the CLI also lets you chat with an LLM
through the terminal, query the API for various model details, and everything
is configurable with extra flags.&lt;/p&gt;
&lt;p&gt;It's &lt;a class="reference external" href="https://github.com/eliben/gemini-cli"&gt;open-source&lt;/a&gt;, of course; the
README file rendered on GitHub has extensive documentation, and more is
available by running &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt; help&lt;/tt&gt;. Try it, ask questions, open issues!&lt;/p&gt;
&lt;hr class="docutils" /&gt;
&lt;table class="docutils footnote" frame="void" id="footnote-1" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label" /&gt;&lt;col /&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="#footnote-reference-1"&gt;[1]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;I like using &lt;a class="reference external" href="https://github.com/eliben/pss/"&gt;pss&lt;/a&gt;, but feel free
to use your favorite tools - &lt;tt class="docutils literal"&gt;git grep&lt;/tt&gt;, &lt;tt class="docutils literal"&gt;ag&lt;/tt&gt; or just a concoction
of &lt;tt class="docutils literal"&gt;find&lt;/tt&gt; and &lt;tt class="docutils literal"&gt;grep&lt;/tt&gt;.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="footnote-2" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label" /&gt;&lt;col /&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="#footnote-reference-2"&gt;[2]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;&lt;p class="first"&gt;A word of caution: LLMs have limited context window sizes; for embeddings,
if the input is larger than the model's context window it may get
truncated - so it's the user's responsibility to ensure that input
documents are properly sized.&lt;/p&gt;
&lt;p class="last"&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt;&lt;/tt&gt; will report the maximal number of input tokens for
supported models when you invoke the &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;gemini-cli&lt;/span&gt; models&lt;/tt&gt; command.&lt;/p&gt;
&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table class="docutils footnote" frame="void" id="footnote-3" rules="none"&gt;
&lt;colgroup&gt;&lt;col class="label" /&gt;&lt;col /&gt;&lt;/colgroup&gt;
&lt;tbody valign="top"&gt;
&lt;tr&gt;&lt;td class="label"&gt;&lt;a class="fn-backref" href="#footnote-reference-3"&gt;[3]&lt;/a&gt;&lt;/td&gt;&lt;td&gt;We have to be careful with too much parallelism, because at the free
tier the Gemini SDK may be rate-limited.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
</content><category term="misc"></category><category term="Go"></category><category term="Machine Learning"></category><category term="Software &amp; Tools"></category></entry><entry><title>static-server: an HTTP server in Go for static content</title><link href="https://eli.thegreenplace.net/2023/static-server-an-http-server-in-go-for-static-content/" rel="alternate"></link><published>2023-09-16T06:20:00-07:00</published><updated>2024-02-25T14:36:58-08:00</updated><author><name>Eli Bendersky</name></author><id>tag:eli.thegreenplace.net,2023-09-16:/2023/static-server-an-http-server-in-go-for-static-content/</id><summary type="html">&lt;p&gt;I put together a simple static file server in Go - useful for local testing
of web applications. Check it out at &lt;a class="reference external" href="https://github.com/eliben/static-server"&gt;https://github.com/eliben/static-server&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you have Go installed on your machine, you don't have to download anything
else; you can run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ go run github.com/eliben/static-server …&lt;/pre&gt;&lt;/div&gt;</summary><content type="html">&lt;p&gt;I put together a simple static file server in Go - useful for local testing
of web applications. Check it out at &lt;a class="reference external" href="https://github.com/eliben/static-server"&gt;https://github.com/eliben/static-server&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you have Go installed on your machine, you don't have to download anything
else; you can run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ go run github.com/eliben/static-server@latest
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And it will start serving the current directory! Run it with &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;-help&lt;/span&gt;&lt;/tt&gt; for
usage information. No configuration files needed - the default is useful
and you can adjust it to your needs using command-line flags.&lt;/p&gt;
&lt;p&gt;Obviously, you can also install it once with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ go install github.com/eliben/static-server@latest
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And then just invoke &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;static-server&lt;/span&gt;&lt;/tt&gt; if your &lt;tt class="docutils literal"&gt;PATH&lt;/tt&gt; is properly set up.&lt;/p&gt;
&lt;div class="section" id="why"&gt;
&lt;h2&gt;Why&lt;/h2&gt;
&lt;p&gt;When developing web applications locally, for basic test cases we can
open an HTML file directly in the browser (using the &lt;a class="reference external" href="https://en.wikipedia.org/wiki/File_URI_scheme"&gt;file:/// scheme&lt;/a&gt;). However, this is sometimes
insufficient, and in several scenarios it's necessary to properly &lt;em&gt;serve&lt;/em&gt; the
HTML (along with its JS and CSS). Some cases where I encountered this are web
applications that use at least one of:&lt;/p&gt;
&lt;ul class="simple"&gt;
&lt;li&gt;Web workers&lt;/li&gt;
&lt;li&gt;Web sockets&lt;/li&gt;
&lt;li&gt;WASM&lt;/li&gt;
&lt;li&gt;Separate API servers, requiring &lt;a class="reference external" href="https://eli.thegreenplace.net/2023/introduction-to-cors-for-go-programmers/"&gt;CORS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Loading ES modules from separate files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the past, when I was more active in the Python ecosystem, I used
&lt;tt class="docutils literal"&gt;python &lt;span class="pre"&gt;-m&lt;/span&gt; SimpleHTTPServer &amp;lt;port&amp;gt;&lt;/tt&gt; quite a bit. While it's nice, it has some
issues too: it's not very configurable, and it requires Python to be installed.&lt;/p&gt;
&lt;p&gt;Another option I've used is &lt;a class="reference external" href="https://www.npmjs.com/package/http-server"&gt;http-server&lt;/a&gt; from the Node.js ecosystem; in
fact, it has served as the inspiration for &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;static-server&lt;/span&gt;&lt;/tt&gt;. You can run it
with &lt;tt class="docutils literal"&gt;npx&lt;/tt&gt; without installing, and it's also configurable through command-line
flags, without requiring configuration files.&lt;/p&gt;
&lt;p&gt;But we can't expect all Go developers to have &lt;tt class="docutils literal"&gt;npm&lt;/tt&gt; or &lt;tt class="docutils literal"&gt;npx&lt;/tt&gt; installed.
Moreover, sometimes you want to tweak the server a bit and digging in JavaScript
is not any Go programmer's idea of a good time. Like many tools in that
ecosystem, this Node.js-based HTTP server is all in on dependencies - with 13
of them, it's not easy to understand or modify its code; much of it is split
across multiple helper packages, and making changes can be tricky.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="how"&gt;
&lt;h2&gt;How&lt;/h2&gt;
&lt;p&gt;Spinning up a static file server in Go is very easy - I wrote a
&lt;a class="reference external" href="https://eli.thegreenplace.net/2022/serving-static-files-and-web-apps-in-go/"&gt;whole blog post&lt;/a&gt; about the
possibilities at some point. The simplest static server to serve the current
working directory is just:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;net/http&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;:8080&amp;quot;&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FileServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Having found myself plopping a small &lt;tt class="docutils literal"&gt;server.go&lt;/tt&gt; with these contents in too
many web projects, I decided enough was enough.
Thus &lt;a class="reference external" href="https://github.com/eliben/static-server"&gt;static-server&lt;/a&gt; was born.&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;static-server&lt;/span&gt;&lt;/tt&gt; is simple, yet versatile. It will do the right thing
by default, with no flags whatsoever. But you can also use flags to configure
a few aspects, e.g.: the port it serves on, CORS support, serving via TLS,
control how logging is done.&lt;/p&gt;
&lt;p&gt;&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;static-server&lt;/span&gt;&lt;/tt&gt; is hackable and easy to understand. All the code is in a
single file (with fewer than 200 lines of code, including comments and handling
flags) and there are &lt;em&gt;no dependencies&lt;/em&gt; (except one package that is only used
for testing).&lt;/p&gt;
&lt;p&gt;I find &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;static-server&lt;/span&gt;&lt;/tt&gt; very useful, and I hope others will too. If you run
into any problems or have questions, open a GitHub issue or send me an email.&lt;/p&gt;
&lt;/div&gt;
</content><category term="misc"></category><category term="Go"></category><category term="Network Programming"></category><category term="Software &amp; Tools"></category></entry><entry><title>Reverse proxying a sub-domain via Apache</title><link href="https://eli.thegreenplace.net/2023/reverse-proxying-a-sub-domain-via-apache/" rel="alternate"></link><published>2023-01-21T06:26:00-08:00</published><updated>2024-05-04T19:46:23-07:00</updated><author><name>Eli Bendersky</name></author><id>tag:eli.thegreenplace.net,2023-01-21:/2023/reverse-proxying-a-sub-domain-via-apache/</id><summary type="html">&lt;p&gt;Suppose you have a domain that hosts your website: &lt;tt class="docutils literal"&gt;domain.com&lt;/tt&gt;, and the
website is served with the venerable &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Apache_HTTP_Server"&gt;Apache HTTP server&lt;/a&gt;. Suppose, also, that you
want to run some backend application on the same domain, perhaps using a
sub-domain like &lt;tt class="docutils literal"&gt;sub.domain.com&lt;/tt&gt;. Running an application on a non-standard …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Suppose you have a domain that hosts your website: &lt;tt class="docutils literal"&gt;domain.com&lt;/tt&gt;, and the
website is served with the venerable &lt;a class="reference external" href="https://en.wikipedia.org/wiki/Apache_HTTP_Server"&gt;Apache HTTP server&lt;/a&gt;. Suppose, also, that you
want to run some backend application on the same domain, perhaps using a
sub-domain like &lt;tt class="docutils literal"&gt;sub.domain.com&lt;/tt&gt;. Running an application on a non-standard
port (not 80 or 443) is not a problem, but what it you need it to run on port
80? Apache occupies port 80 in order to serve &lt;tt class="docutils literal"&gt;domain.com&lt;/tt&gt;, so at least on
the surface this seems like a problem.&lt;/p&gt;
&lt;img alt="Logo of the Apache HTTP server project" class="align-center" src="https://eli.thegreenplace.net/images/2023/apache-logo.png" style="width: 500px;" /&gt;
&lt;p&gt;This post talks about how to make it work using the reverse-proxying
capabilities of Apache. It assumes you control a virtual machine that has
a top-level domain like &lt;tt class="docutils literal"&gt;domain.com&lt;/tt&gt; mapped to it, and that the machine runs
Linux.&lt;/p&gt;
&lt;div class="section" id="setting-up-apache-as-a-proxy-with-mod-proxy"&gt;
&lt;h2&gt;Setting up Apache as a proxy with mod_proxy&lt;/h2&gt;
&lt;p&gt;If you need to brush up on proxy concepts, consider reading &lt;a class="reference external" href="https://eli.thegreenplace.net/2022/go-and-proxy-servers-part-1-http-proxies/"&gt;this series of
posts&lt;/a&gt;
first.&lt;/p&gt;
&lt;p&gt;Assuming Apache is already installed and running on the server, you'll first
have to enable the &lt;a class="reference external" href="https://httpd.apache.org/docs/2.4/mod/mod_proxy.html"&gt;proxy module&lt;/a&gt; and restart the
service:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ sudo a2enmod proxy proxy_http
$ sudo systemctl restart apache2
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Sub-domains typically have their own configuration file in
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;/etc/apache2/sites-available&lt;/span&gt;&lt;/tt&gt;. Create a new configuration file in
that directory, named &lt;tt class="docutils literal"&gt;sub.domain.com.conf&lt;/tt&gt; or some such; here's what should
be in it (adjust as needed):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&amp;lt;VirtualHost *:80&amp;gt;
        ProxyPreserveHost On
        ProxyPass / http://127.0.0.1:5000/
        ProxyPassReverse / http://127.0.0.1:5000/

        ServerName sub.domain.com
        ServerAdmin your@email.com

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined
&amp;lt;/VirtualHost&amp;gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This tells Apache that the &lt;tt class="docutils literal"&gt;sub.domain.com&lt;/tt&gt; route should be proxied to a
service running locally on port 5000; naturally, the service address can have a
different port or run on a different domain altogether.&lt;/p&gt;
&lt;p&gt;Next you'll want to register that configuration with Apache and restart it
again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ sudo a2ensite sub.domain.com.conf
$ sudo systemctl restart apache2
&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="section" id="running-the-backend-service"&gt;
&lt;h2&gt;Running the backend service&lt;/h2&gt;
&lt;p&gt;Now that Apache is all set up, it's time to run the actual backend service at
port 5000. As an example, you can run
&lt;a class="reference external" href="https://github.com/eliben/code-for-blog/blob/main/2022/go-and-proxies/http-server-debug-request-headers.go"&gt;this simple header debugging server&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ go run http-server-debug-request-headers.go -addr 127.0.0.1:5000
2023/01/17 01:01:20 Starting server on 127.0.0.1:5000
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To test that it runs properly, in a separate terminal (on the same machine!)
let's run &lt;tt class="docutils literal"&gt;curl&lt;/tt&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ curl 127.0.0.1:5000/headers
hello /headers
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And looking at the terminal where the server is running, you should see some
useful logging:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;2023/01/17 01:02:50 127.0.0.1:42406   GET     /headers        Host: 127.0.0.1:5000
User-Agent: curl/7.81.0
Accept: */*
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you've followed all the steps in this and the previous session, it should
work via the sub-domain now (from any machine):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ curl http://sub.domain.com/headers
hello /headers
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Apache listens on port 80 for &lt;tt class="docutils literal"&gt;domain.com&lt;/tt&gt;, and when it sees requests to
&lt;tt class="docutils literal"&gt;sub.domain.com&lt;/tt&gt;, it proxies them to the server running on port &lt;tt class="docutils literal"&gt;5000&lt;/tt&gt; on
the same machine.&lt;/p&gt;
&lt;p&gt;If this doesn't work for you, take a careful look at the Apache logs - both
the error log and the access log may be useful.&lt;/p&gt;
&lt;/div&gt;
&lt;div class="section" id="bonus-tls-with-let-s-encrypt"&gt;
&lt;h2&gt;Bonus: TLS with Let's Encrypt&lt;/h2&gt;
&lt;p&gt;If your server is set up to serve &lt;tt class="docutils literal"&gt;domain.com&lt;/tt&gt; via TLS using Let's Encrypt,
I have good news for you -- it will &lt;em&gt;just work&lt;/em&gt; for &lt;tt class="docutils literal"&gt;sub.domain.com&lt;/tt&gt; as well!&lt;/p&gt;
&lt;p&gt;Presumably you've set up Let's Encrypt certificates using &lt;tt class="docutils literal"&gt;certbot&lt;/tt&gt;. Since
we've now added an additional Apache configuration (&lt;tt class="docutils literal"&gt;sub.domain.com.conf&lt;/tt&gt;), we
should run &lt;tt class="docutils literal"&gt;certbot&lt;/tt&gt; again:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ sudo certbot --apache
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And carefully follow the on-screen instructions. &lt;tt class="docutils literal"&gt;certbot&lt;/tt&gt; should detect
there's a new sub-domain to get a certificate for; if everything goes as
expected, it succeeds and from that point on you should be able to access the
backend server via HTTPS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;$ curl https://sub.domain.com/headers
hello /headers
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note that the backend Go server serves HTTP; the reverse proxy (Apache)
terminates the TLS connection and passes HTTP to the backend server. This is
a fairly common way to structure backends. While the backend server
serves unencrypted traffic, it's not actually accessible from outside the
machine (port 5000 is unlikely to be exposed). The only way to access it is
via the reverse-proxy on &lt;tt class="docutils literal"&gt;sub.domain.com&lt;/tt&gt;, which can use TLS if needed.&lt;/p&gt;
&lt;p&gt;I was wondering how this works. &lt;tt class="docutils literal"&gt;certbot&lt;/tt&gt; uses the HTTP challenge with Let's
Encrypt, wherein it's asked to serve a special file on a special path
(typically something like &lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;.well-known/acme-challenge&lt;/span&gt;&lt;/tt&gt;) to prove to Let's
Encrypt that it controls the domain. But here all requests get forwarded to
the backend server...&lt;/p&gt;
&lt;p&gt;After scratching my head for a minute I found the answer in &lt;tt class="docutils literal"&gt;certbot&lt;/tt&gt;'s logs,
where it honestly explains its tricky ways. It turns out it adds a
&lt;tt class="docutils literal"&gt;RewriteRule&lt;/tt&gt; to our &lt;tt class="docutils literal"&gt;sub.domain.com.conf&lt;/tt&gt; file for the duration of the
Let's Encrypt handshake, sending any requests starting with
&lt;tt class="docutils literal"&gt;&lt;span class="pre"&gt;.well-known/acme-challenge&lt;/span&gt;&lt;/tt&gt; to a known disk location it controls. After all
is done, it quietly removes these rules from the configuration file.&lt;/p&gt;
&lt;/div&gt;
</content><category term="misc"></category><category term="Network Programming"></category><category term="Internet"></category><category term="Software &amp; Tools"></category></entry></feed>