<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Duarte O.Carmo</title><link href="https://duarteocarmo.com/" rel="alternate"/><link href="https://duarteocarmo.com/feed.xml" rel="self"/><id>https://duarteocarmo.com/</id><updated>2026-08-30T00:00:00+02:00</updated><entry><title>How I use Pi</title><link href="https://duarteocarmo.com/blog/how-i-use-pi.html" rel="alternate"/><published>2026-08-30T00:00:00+02:00</published><updated>2026-08-30T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-08-30:/blog/how-i-use-pi.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/111/pi-tools-banner.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/111/pi-tools-banner.webp" alt="pi-tools banner" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;Today, I'd like to talk about &lt;a href="https://pi.dev/"&gt;Pi&lt;/a&gt;. Pi is a minimal coding agent originally built by &lt;a href="https://mariozechner.at/"&gt;Mario Zechner&lt;/a&gt;. It's one of the key tools I use to get things done. Brainstorming, writing code, testing, debugging, navigating, research, you name it. The idea of Pi is simple: ship the minimum amount …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/111/pi-tools-banner.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/111/pi-tools-banner.webp" alt="pi-tools banner" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;Today, I'd like to talk about &lt;a href="https://pi.dev/"&gt;Pi&lt;/a&gt;. Pi is a minimal coding agent originally built by &lt;a href="https://mariozechner.at/"&gt;Mario Zechner&lt;/a&gt;. It's one of the key tools I use to get things done. Brainstorming, writing code, testing, debugging, navigating, research, you name it. The idea of Pi is simple: ship the minimum amount of tools and let the agent evolve with its user (i.e. me!).&lt;/p&gt;
&lt;h2 id="but-why-pi"&gt;But why Pi?&lt;/h2&gt;
&lt;p&gt;I've used most agent harnesses out there. &lt;a href="https://www.anthropic.com/claude-code"&gt;Claude Code&lt;/a&gt;, &lt;a href="https://openai.com/codex/"&gt;Codex&lt;/a&gt;, &lt;a href="https://opencode.ai/"&gt;OpenCode&lt;/a&gt;, you name it. None of them really stuck like Pi did. Some are closed source (even &lt;a href="https://www.infoq.com/news/2026/04/claude-code-source-leak/"&gt;though their code got leaked&lt;/a&gt;); others tend to change under my feet. Pi is simple, predictable, and doesn't do anything I'm not expecting. For something I drive every day, reliability is important. I think I'm also largely aligned with the philosophy of Pi.&lt;/p&gt;
&lt;p&gt;Importantly, Pi is also model agnostic. It's not tied to a frontier lab or model provider. &lt;a href="https://earendil.com/posts/session-portability/"&gt;Working for every model out there is increasingly challenging&lt;/a&gt;, but Pi does a great job. I can use my Codex sub for some work, DeepSeek Flash V4 through the API for other work, and my local &lt;a href="https://github.com/huggingface/pi-llama"&gt;llama.cpp&lt;/a&gt; models when I don't need &lt;em&gt;frontier intelligence&lt;/em&gt;.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/111/pi-helicopter.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/111/pi-helicopter.webp" alt="pi-helicopter menu over a blue wallpaper" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;pi-helicopter is one of the extensions that I've built for Pi, more below&lt;/figcaption&gt;
&lt;/center&gt;

&lt;h2 id="my-workflow"&gt;My workflow&lt;/h2&gt;
&lt;p&gt;My everyday workflow revolves largely around the terminal. My Mac is my main machine, with a &lt;a href="https://duarteocarmo.com/blog/how-i-self-host-in-2024.html"&gt;Hetzner server&lt;/a&gt; for &lt;a href="https://duarteocarmo.com/blog/how-i-hermes.html"&gt;Hermes&lt;/a&gt; and agents I want to run in the cloud. Everything is connected to the same &lt;a href="https://tailscale.com/"&gt;Tailscale&lt;/a&gt; network. &lt;a href="https://ghostty.org/"&gt;Ghostty&lt;/a&gt; makes everything pretty seamless, together with &lt;a href="https://github.com/tmux/tmux/wiki"&gt;Tmux&lt;/a&gt;, &lt;a href="https://neovim.io/"&gt;Neovim&lt;/a&gt;, and &lt;a href="https://github.com/jesseduffield/lazygit"&gt;LazyGit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Tmux maps well to my mental model of work. Every project gets its own session, every task gets its own window. Nowadays, most of the time, I'll be running 2-3 agents in parallel on smaller things while I put my focus on a single one.&lt;/p&gt;
&lt;p&gt;A few weeks ago I &lt;a href="https://duarteocarmo.com/blog/retrospectiva-10.html"&gt;mentioned I stopped using Tmux in favour of Herdr&lt;/a&gt;. But I switched back. In &lt;a href="https://herdr.dev/"&gt;Herdr&lt;/a&gt;, you get "pinged" when an agent is done. You are constantly looking at the status of all your agents. That makes me tired. I want to check on my agents, but at my own rhythm. I don't want to be pinged. I like Do Not Disturb mode too much. The cognitive load was a bit too much. I needed another solution.&lt;/p&gt;
&lt;center&gt;
&lt;video style="display:block;max-width:100%;border-radius: 2px" autoplay loop muted playsinline controls&gt;
  &lt;source src="https://duarteocarmo.com/images/111/pi-jumper-demo.mp4" type="video/mp4"&gt;
&lt;/video&gt;
&lt;figcaption style="margin-top:0"&gt;pi-jumper allows you to jump between Pi sessions quickly&lt;/figcaption&gt;
&lt;/center&gt;

&lt;h2 id="pi-tools-minimal-tools-for-pi"&gt;Pi-tools, minimal tools for Pi&lt;/h2&gt;
&lt;p&gt;I ended up building &lt;a href="https://github.com/duarteocarmo/pi-tools/"&gt;pi-tools&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;pi-tools is a set of tools I use as part of my Pi workflow. The centerpiece (and probably the only &lt;em&gt;really&lt;/em&gt; original tool) is &lt;a href="https://github.com/duarteocarmo/pi-tools/tree/master/packages/pi-jumper"&gt;&lt;code&gt;pi-jumper&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;pi-jumper is my own take on the whole "multiple agents running, Herdr, Commander, your agent pings you when done, parallel work bullshit". It has two widgets: a sticky one below your input field that shows you how many other Pi sessions you have running (which you can turn off). The second allows you to quickly jump to other Pi sessions running on Tmux.&lt;/p&gt;
&lt;p&gt;But pi-tools has a couple of other goodies.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/duarteocarmo/pi-tools/tree/master/packages/pi-helicopter"&gt;pi-helicopter&lt;/a&gt; is something I completely ripped off from &lt;a href="https://github.com/phun333/pi-infobar"&gt;phun333's Pi-infobar app&lt;/a&gt;. This one is Mac-only. Pi-infobar was &lt;a href="https://github.com/phun333/pi-infobar/pull/10"&gt;consuming 1 GB of RAM on my Mac&lt;/a&gt;. pi-helicopter is a bit more lightweight and performant, for now at least.&lt;/p&gt;
&lt;p&gt;The rest of the pi-tools extensions are small things I've built that make my life a bit more pleasant. &lt;a href="https://github.com/duarteocarmo/pi-tools/tree/master/packages/pi-no-sleep"&gt;Pi-no-sleep&lt;/a&gt; prevents my Mac from sleeping while Pi is running. &lt;a href="https://github.com/duarteocarmo/pi-tools/tree/master/packages/pi-preview"&gt;Pi-preview&lt;/a&gt; allows me to read longer clanker messages in a nice web page. &lt;a href="https://github.com/duarteocarmo/pi-tools/tree/master/packages/pi-subagents"&gt;Pi-subagents&lt;/a&gt; is a very minimal implementation of subagents for Pi I stole from &lt;a href="https://github.com/mitsuhiko/agent-stuff/blob/main/extensions/subagent.ts"&gt;Armin&lt;/a&gt;, and &lt;a href="https://github.com/duarteocarmo/pi-tools/tree/master/packages/pi-modus-themes"&gt;Pi-modus themes&lt;/a&gt; is a port of my favourite themes for Pi.&lt;/p&gt;
&lt;center&gt;
&lt;video style="display:block;max-width:100%;border-radius: 2px" autoplay loop muted playsinline controls&gt;
  &lt;source src="https://duarteocarmo.com/images/111/pi-subagents-demo.mp4" type="video/mp4"&gt;
&lt;/video&gt;
&lt;figcaption style="margin-top:0"&gt;Simple subagents with pi-subagents&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;All of these extensions tend to evolve with my taste. I believe they all stick to Pi's philosophy of being minimal instead of overwhelming.&lt;/p&gt;
&lt;p&gt;And that's the idea of Pi, no? Minimal, extensible, but still predictable.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Note: I don't use just pi-tools, I use a couple of other extensions and skills. Check &lt;a href="https://github.com/duarteocarmo/dotfiles"&gt;my dotfiles for more details&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Bagaço v3: Now with PDFs and Wikipedia!</title><link href="https://duarteocarmo.com/blog/bagaco-v3-now-with-pdfs-and-wikipedia.html" rel="alternate"/><published>2026-08-25T00:00:00+02:00</published><updated>2026-08-25T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-08-25:/blog/bagaco-v3-now-with-pdfs-and-wikipedia.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/110/source-comparison.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/110/source-comparison.webp" alt="Bagaço v3 sources compared by document share, average document length, and average educational score" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;Source composition and averages.&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;I'm proud to announce &lt;a href="https://huggingface.co/datasets/duarteocarmo/bagaco3"&gt;Bagaço v3&lt;/a&gt;, the third version of the largest European Portuguese pretraining dataset for large language models.&lt;/p&gt;
&lt;p&gt;Why a new version? After my work on &lt;a href="https://duarteocarmo.com/blog/ginjinha-pretraining-llms-on-european-portuguese.html"&gt;Ginjinha&lt;/a&gt;, one thing became pretty apparent: &lt;a href="https://duarteocarmo.com/blog/bagaco-a-pretraining-dataset-for-european-portuguese.html"&gt;Bagaço&lt;/a&gt; was large and diverse, but lacked high-quality data. I believe Bagaço …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/110/source-comparison.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/110/source-comparison.webp" alt="Bagaço v3 sources compared by document share, average document length, and average educational score" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;Source composition and averages.&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;I'm proud to announce &lt;a href="https://huggingface.co/datasets/duarteocarmo/bagaco3"&gt;Bagaço v3&lt;/a&gt;, the third version of the largest European Portuguese pretraining dataset for large language models.&lt;/p&gt;
&lt;p&gt;Why a new version? After my work on &lt;a href="https://duarteocarmo.com/blog/ginjinha-pretraining-llms-on-european-portuguese.html"&gt;Ginjinha&lt;/a&gt;, one thing became pretty apparent: &lt;a href="https://duarteocarmo.com/blog/bagaco-a-pretraining-dataset-for-european-portuguese.html"&gt;Bagaço&lt;/a&gt; was large and diverse, but lacked high-quality data. I believe Bagaço v3 fixes that.&lt;/p&gt;
&lt;p&gt;This new version builds on top of &lt;a href="https://duarteocarmo.com/blog/the-largest-open-pretraining-dataset-for-european-portuguese.html"&gt;Bagaço v2&lt;/a&gt; and the great work from the Hugging Face team, and adds documents from Wikipedia (&lt;a href="https://huggingface.co/datasets/HuggingFaceFW/finewiki"&gt;FineWiki&lt;/a&gt;) and from PDFs across the web (&lt;a href="https://huggingface.co/datasets/HuggingFaceFW/finepdfs"&gt;FinePDFs&lt;/a&gt;). But that's not the full story. Let's get into the details.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/110/sources-table.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/110/sources-table.webp" alt="Bagaço v3 source statistics table" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;Bagaço v3 sources.&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;To create Bagaço v3, I started by using &lt;a href="https://github.com/huggingface/datatrove"&gt;DataTrove&lt;/a&gt; to build a pipeline that merged Bagaço v2 (originally from FineWeb2), FineWiki, and FinePDFs. It filters the last two by European Portuguese score (using &lt;a href="https://huggingface.co/duarteocarmo/fasttext-euptvid"&gt;my own classifier&lt;/a&gt;, which I &lt;a href="https://duarteocarmo.com/blog/portuguese-variety-identification-the-bitter-lesson.html"&gt;wrote about here&lt;/a&gt;), and deduplicates everything using MinHash deduplication.&lt;/p&gt;
&lt;p&gt;&lt;iframe
  src="https://huggingface.co/datasets/duarteocarmo/bagaco3/embed/viewer/default/train"
  frameborder="0"
  width="100%"
  height="560px"
&gt;&lt;/iframe&gt;
&lt;/p&gt;
&lt;p&gt;But we don't want to dump everything into the same place just like that. Just like Bagaço v2, v3 adds two dimensions to this dataset: &lt;strong&gt;educational score&lt;/strong&gt; (from 0 to 5) and &lt;strong&gt;category&lt;/strong&gt; (Arts, Business, Games, etc.). To get this done, I used Gemini 3.7 Flash and OpenRouter's batch API and started by annotating 36K documents with the following prompt:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Below is an extract from a web page. Evaluate whether the page has a high educational value
and could be useful in an educational setting for teaching from primary school to grade school
levels using the additive 5-point scoring system described below. The text will be in Portuguese.
Evaluate its educational value based on content quality, not language.

- 1 point: basic information relevant to educational topics, even with ads/promotional material.
- 2 points: addresses elements pertinent to education but doesn&amp;#39;t align closely with standards.
- 3 points: appropriate for educational use, introduces key concepts relevant to school curricula.
- 4 points: highly relevant for grade school education, clear writing, substantial content.
- 5 points: outstanding educational value, perfectly suited for primary/grade school teaching.

The extract:
&amp;lt;extract&amp;gt;
{document_text}
&amp;lt;/extract&amp;gt;

After examining the extract, briefly justify your total score (up to 100 words)
and provide the educational score (0-5). Also classify the extract into one category: Society,
Arts, Business, Science, Sports, Lifestyle, Health, Games, or News.

Respond ONLY with a JSON object in this format:
{&amp;quot;justification&amp;quot;: &amp;quot;up to 100 words&amp;quot;, &amp;quot;educational_score&amp;quot;: 0, &amp;quot;category&amp;quot;: &amp;quot;category&amp;quot;}

Examples of good responses:
{&amp;quot;justification&amp;quot;: &amp;quot;The extract clearly explains photosynthesis and its role in plant growth, with useful scientific concepts for students.&amp;quot;, &amp;quot;educational_score&amp;quot;: 4, &amp;quot;category&amp;quot;: &amp;quot;Science&amp;quot;}
{&amp;quot;justification&amp;quot;: &amp;quot;The extract is mainly a product listing and offers little explanation beyond basic promotional information.&amp;quot;, &amp;quot;educational_score&amp;quot;: 1, &amp;quot;category&amp;quot;: &amp;quot;Lifestyle&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With that annotated dataset, I used the &lt;em&gt;classic&lt;/em&gt; embeddings + &lt;code&gt;LogisticRegression&lt;/code&gt; to &lt;a href="https://huggingface.co/datasets/duarteocarmo/bagaco3/tree/main/classifier"&gt;train a classifier and apply it to the whole dataset&lt;/a&gt;. 4× RTX 4090s and 2 hours later, I had the entire dataset annotated. After some testing and many experiments with &lt;a href="https://github.com/davebcn87/pi-autoresearch"&gt;pi-autoresearch&lt;/a&gt;, an extension inspired by Karpathy's autoresearch project, I didn't find anything that really beat it.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/110/label-distributions.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/110/label-distributions.webp" alt="Bagaço v3 documents and characters by predicted category and educational score" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;Documents and characters by predicted label.&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;Bagaço v3 adds ~2 million documents to the 32M documents from Bagaço. Why the effort for a 7% increase? If you look closely - even though we only add 2M documents, the character increase is around 40% (!). And that might very well translate into many more tokens. Bagaço v3 has an estimated 29 billion (!) tokens. And hopefully, many more high-quality ones.&lt;/p&gt;
&lt;p&gt;And there we go - a &lt;a href="https://huggingface.co/datasets/duarteocarmo/bagaco3"&gt;shiny new dataset&lt;/a&gt;. Now all we have to do is train on it (again).&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Retrospectiva #10</title><link href="https://duarteocarmo.com/blog/retrospectiva-10.html" rel="alternate"/><published>2026-08-10T00:00:00+02:00</published><updated>2026-08-10T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-08-10:/blog/retrospectiva-10.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/109/banner.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/109/banner.webp" alt="Hiking with Allegra in the mountains" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;July was busy — like always — but we made sure to take some time and touch some grass. Or mountains in our case. With the high temperatures across Europe we went to the Trentino region and hiked around the Dolomites for five days. Cool temperatures and beautiful scenery. Vitto's idea  of …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/109/banner.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/109/banner.webp" alt="Hiking with Allegra in the mountains" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;July was busy — like always — but we made sure to take some time and touch some grass. Or mountains in our case. With the high temperatures across Europe we went to the Trentino region and hiked around the Dolomites for five days. Cool temperatures and beautiful scenery. Vitto's idea  of course.&lt;/p&gt;
&lt;p&gt;Work is busy - but very fun as always. Allegra is growing fast which is incredible to see (while trying to fight sleep debt as much as we can).&lt;/p&gt;
&lt;h2 id="using"&gt;Using&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://herdr.dev/" target="_blank"&gt;herdr&lt;/a&gt;&lt;/strong&gt;: Never thought I'd see the day where something would replace Tmux for me. Tmux is &lt;em&gt;ingrained&lt;/em&gt; in my muscle memory (a bit like Vim). Tmux is just the way I've always worked. When my friend &lt;a href="https://x.com/strickvl" target="_blank"&gt;Alex&lt;/a&gt; asked me if I was already in the herdr train, I told him no, of course - why should I? Until I realised I used multiple agents, but always had to keep checking if they were done. Not anymore. Oh, and the tmux-like bindings also help. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.outdoorsupply.co.uk/child-carrier-bergans-kids-lilletind-grey" target="_blank"&gt;Bergans Kids Lilletind Child Carrier&lt;/a&gt;&lt;/strong&gt;: There's no way we could've done 50 KM+ of hiking with an 8 month old without this thing. Vitto bought it used in Denmark and Allegra loved it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/ggml-org/Llama-macOS" target="_blank"&gt;Llama macOS&lt;/a&gt;&lt;/strong&gt;: Local LLMs are here to stay. I'm convinced we won't need frontier intelligence (e.g., Sol or Fable) for every single thing under the sun. I want my experience when running local LLMs to be simple, but powerful. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://organicmaps.app" target="_blank"&gt;Organic Maps&lt;/a&gt;&lt;/strong&gt;: I use a Garmin watch. But I hate the Garmin Connect app: slow, buggy, and just too much. You want to follow a trail, you have a GPX file for it, and just want to follow it: Garmin Connect complicates things, Organic Maps doesn't. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.rsshub.app/routes/youtube" target="_blank"&gt;RSSHub&lt;/a&gt;&lt;/strong&gt;: RSS is how I read news. I follow hundreds of websites on my &lt;a href="https://duarteocarmo.com/blog/drowning-in-news.html"&gt;self-hosted FreshRSS&lt;/a&gt; and use &lt;a href="https://netnewswire.com" target="_blank"&gt;NetNewsWire&lt;/a&gt; to read them. Everyone is (still) actively trying to force you to go to their website even though I don't want to. I want to use RSS. For Reddit, I found a solution: I use &lt;a href="https://github.com/johnwarne/reddit-top-rss" target="_blank"&gt;toprss&lt;/a&gt;. Now, with RSSHub I can do the same for Youtube channels. I like having all the things in the same place. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/manalkaff/opendesign" target="_blank"&gt;OpenDesign&lt;/a&gt;&lt;/strong&gt;: You like Claude Design, I like Claude Design, but I don't like having an Anthropic subscription. OpenDesign solves just that. It's a Claude Design inspired product, but it's (1) open-source, and (2) you can use it with any coding agent/model under the sun. Your vibe coded presentation won't be pristine - but it will get you 80% of the way there, and allow you to iterate on a bunch of potential good designs.&lt;/p&gt;
&lt;h2 id="reading"&gt;Reading&lt;/h2&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/109/laguna-workflow-and-sampling.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/109/laguna-workflow-and-sampling.webp" alt="Laguna web-data workflow and sampling distribution" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt; Laguna Pre-training
(&lt;a href="https://poolside.ai/assets/laguna/laguna-m1-xs2-technical-report.pdf" target="_blank"&gt;Source&lt;/a&gt;)
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://austinkleon.com/show-your-work/" target="_blank"&gt;Show Your Work!&lt;/a&gt;&lt;/strong&gt;: A nice read for those who like doing creative work. How to build an audience, why having your website is important. Not life changing - but light, well written, and inspiring. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://poolside.ai/assets/laguna/laguna-m1-xs2-technical-report.pdf" target="_blank"&gt;Laguna M.1/XS.2 Technical Report&lt;/a&gt;&lt;/strong&gt;: One of the most well-written technical reports I've read in a while. Doesn't tell you &lt;em&gt;all&lt;/em&gt; the secrets, but gives you enough detail to be really educational. Software, pre-training, SFT, infrastructure, RL, what went &lt;em&gt;wrong&lt;/em&gt; during training.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://arxiv.org/abs/2410.00037" target="_blank"&gt;Moshi: A speech-text foundation model for real-time dialogue&lt;/a&gt;&lt;/strong&gt;: How do you build a full-duplex model? Something that can speak and listen just like we do? Moshi is an &lt;em&gt;oldish&lt;/em&gt; paper but goes into exactly that. Also recommend the &lt;a href="https://arxiv.org/pdf/2509.08753" target="_blank"&gt;DSM paper&lt;/a&gt; from the same lab.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.manning.com/books/build-a-reasoning-model-from-scratch" target="_blank"&gt;How to Build a Reasoning Model from Scratch&lt;/a&gt;&lt;/strong&gt;: I've read almost all of Raschka's books. I love the way he writes. Onto his latest.&lt;/p&gt;
&lt;h2 id="listening"&gt;Listening&lt;/h2&gt;
&lt;iframe style="border-radius:12px" src="https://open.spotify.com/embed/album/46mboRimEXi5KPeUqtkC2i?utm_source=generator" width="100%" height="352" frameborder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"&gt;&lt;/iframe&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://music.apple.com/us/album/whacks-museum/6779443177" target="_blank"&gt;WHACK'S MUSEUM — Tierra Whack&lt;/a&gt;&lt;/strong&gt;: Easily the most I've enjoyed a hip-hop/rap album in the past five years. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://open.spotify.com/prerelease/2xj634NfiKTHdKQvfaEmSv" target="_blank"&gt;Feel Forever — Tokyo Tea Room&lt;/a&gt;&lt;/strong&gt;: My favourite band of the moment has been dropping singles from their upcoming album. And they are all bangers.&lt;/p&gt;
&lt;h2 id="watching"&gt;Watching&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.fxnetworks.com/shows/welcome-to-wrexham" target="_blank"&gt;Welcome to Wrexham&lt;/a&gt;&lt;/strong&gt;: There's just something sports documentaries do for me that no other genre does. I think it's the storytelling. I love everything about this show. From the way it's produced, to the storytelling, to pulling me into a world I know nothing about. &lt;/p&gt;
&lt;p&gt;Wherever you are this summer. Remember to recharge and touch some grass.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Ginjinha: Pretraining LLMs on European Portuguese</title><link href="https://duarteocarmo.com/blog/ginjinha-pretraining-llms-on-european-portuguese.html" rel="alternate"/><published>2026-08-06T00:00:00+02:00</published><updated>2026-08-06T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-08-06:/blog/ginjinha-pretraining-llms-on-european-portuguese.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/108/wandb-pt-exams-evolution.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/108/wandb-pt-exams-evolution.webp" alt="Portuguese History and Geography exam accuracy during pretraining for three Ginjinha runs" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;For the past year, I've been very curious about the intersection of Large Language Models (LLMs) and the European Portuguese language. I've built &lt;a href="https://duarteocarmo.com/blog/a-benchmark-for-language-models-on-european-portuguese.html"&gt;evaluations&lt;/a&gt;, &lt;a href="https://duarteocarmo.com/blog/portuguese-variety-identification-the-bitter-lesson.html"&gt;classifiers&lt;/a&gt;, and eventually &lt;a href="https://duarteocarmo.com/blog/the-largest-open-pretraining-dataset-for-european-portuguese.html"&gt;the largest pretraining dataset in European Portuguese&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, that's all fine and dandy, but it raises the obvious question: Can we pretrain an …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/108/wandb-pt-exams-evolution.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/108/wandb-pt-exams-evolution.webp" alt="Portuguese History and Geography exam accuracy during pretraining for three Ginjinha runs" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;For the past year, I've been very curious about the intersection of Large Language Models (LLMs) and the European Portuguese language. I've built &lt;a href="https://duarteocarmo.com/blog/a-benchmark-for-language-models-on-european-portuguese.html"&gt;evaluations&lt;/a&gt;, &lt;a href="https://duarteocarmo.com/blog/portuguese-variety-identification-the-bitter-lesson.html"&gt;classifiers&lt;/a&gt;, and eventually &lt;a href="https://duarteocarmo.com/blog/the-largest-open-pretraining-dataset-for-european-portuguese.html"&gt;the largest pretraining dataset in European Portuguese&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, that's all fine and dandy, but it raises the obvious question: Can we pretrain an LLM fully in European Portuguese? And is it any good?&lt;/p&gt;
&lt;p&gt;Welcome to project Ginjinha.&lt;/p&gt;
&lt;p&gt;But before we start training models, we need a harness.&lt;/p&gt;
&lt;h2 id="a-pretraining-harness-nanochat"&gt;A pretraining harness: NanoChat&lt;/h2&gt;
&lt;p&gt;In 2025, Andrej Karpathy released &lt;a href="https://github.com/karpathy/nanochat"&gt;NanoChat&lt;/a&gt;: a small, hackable codebase for training GPT-2-style language models. It's easy to understand and modify. Exactly what I needed.&lt;/p&gt;
&lt;p&gt;The first step was to adapt it to Ginjinha (&lt;a href="https://github.com/duarteocarmo/nanochat"&gt;code&lt;/a&gt;). NanoChat includes the three key stages of training LLMs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pretraining:&lt;/strong&gt; We train a text-completion model to predict the next token.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Supervised fine-tuning (SFT):&lt;/strong&gt; We further train the model to follow instructions (i.e., chat).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reinforcement learning (RL):&lt;/strong&gt; We "align" the model through preference tuning or verifiable rewards (very hyped nowadays).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We could do all of these, provided we had the data. But Bagaço v2 is a pretraining dataset, so we are only interested in the first stage here. If we use Bagaço to pretrain an LLM, how good a model can we get?&lt;/p&gt;
&lt;p&gt;To measure how good a base model is, NanoChat uses something called the CORE metric.&lt;/p&gt;
&lt;h2 id="from-core-to-ptcore"&gt;From CORE to PTCORE&lt;/h2&gt;
&lt;p&gt;The CORE metric is an evaluation suite designed to measure the capability of a language model on downstream tasks. For CORE, there are 22 tasks. These tasks are things like &lt;a href="https://arxiv.org/abs/1803.05457"&gt;ARC&lt;/a&gt;, &lt;a href="https://arxiv.org/abs/1905.07830"&gt;HellaSwag&lt;/a&gt;, and &lt;a href="https://arxiv.org/abs/2206.04615"&gt;BIG-bench&lt;/a&gt;. Each one is made up of questions (usually multiple choice). We take each answer and measure the loss that the model assigns to it. If the correct answer gets the lowest loss, we consider the model answered correctly.&lt;/p&gt;
&lt;p&gt;A trivial example:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Question:
What is the capital of Le Marche?

Candidate completions:
A. Camerano
   → average loss: 1.76
B. Ancona
   → average loss: 1.12

Prediction: B (lowest loss)
Correct answer: B ✓
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The CORE metric is a great way to evaluate a model if you are building a general-purpose LLM (think ChatGPT).&lt;/p&gt;
&lt;p&gt;For our use case, since we're training on European Portuguese data, we're interested in capabilities that measure knowledge of Portugal and the Portuguese language: enter &lt;strong&gt;PTCORE&lt;/strong&gt;.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/108/ptcore-vs-training-tokens.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/108/ptcore-vs-training-tokens.webp" alt="PTCORE compared with training tokens across all Ginjinha runs" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;PTCORE across training budgets. Bubble size represents the model's total number of parameters.&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;&lt;a href="https://huggingface.co/datasets/duarteocarmo/ptcore-eval"&gt;PTCORE&lt;/a&gt; is a collection of six tasks that measure the capabilities of base models across Portuguese language and culture (e.g., think CORE for Portuguese). It draws on recent work from the &lt;a href="https://duarteocarmo.com/blog/amalia-and-the-future-of-european-portuguese-llms.html"&gt;AMÁLIA team&lt;/a&gt; and a couple of other datasets I curated. Here's the full list:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;What it measures&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/datasets/duarteocarmo/ptcore-eval/viewer/sst2_pt_mini/validation"&gt;SST2-PT&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Sentiment classification in Portuguese&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2,048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/datasets/duarteocarmo/ptcore-eval/viewer/alba_mcq/validation"&gt;ALBA-MCQ&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;European Portuguese linguistics, language variety, and wordplay&lt;/td&gt;
&lt;td style="text-align: right;"&gt;240&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/datasets/duarteocarmo/ptcore-eval/viewer/cultura_viva_pt_mcq/validation"&gt;CulturaVivaPT&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Portuguese culture, places, history, and personalities&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/datasets/duarteocarmo/ptcore-eval/viewer/pt_exams_history_geography/validation"&gt;PT Exams&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;History and Geography questions from Portuguese national exams&lt;/td&gt;
&lt;td style="text-align: right;"&gt;544&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/datasets/duarteocarmo/ptcore-eval/viewer/saudade_pt/validation"&gt;SAUDADE&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Temporal reasoning about Portuguese events&lt;/td&gt;
&lt;td style="text-align: right;"&gt;8,573&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://huggingface.co/datasets/duarteocarmo/ptcore-eval/viewer/openbookqa_mt_pt/validation"&gt;OpenBookQA-PT&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Commonsense and elementary science questions translated into Portuguese&lt;/td&gt;
&lt;td style="text-align: right;"&gt;500&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;And here's an example question from the &lt;code&gt;SAUDADE&lt;/code&gt; task:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Question:
Qual dos dois acontecimentos sobre Fortios ocorreu primeiro?

Candidate completions:
A. Registo populacional de 1785 habitantes em Fortios
   → average loss: 1.76
B. Fortios foi desanexada da freguesia de São Lourenço
   → average loss: 1.12

Prediction: B (lowest loss)
Correct answer: B ✓
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For each task, we can measure two scores: accuracy and a centered score. Accuracy is self-explanatory: how many questions the model got right. The centered score is a bit more interesting: 0 means random performance (for example, 33% accuracy on a multiple-choice question with three options gives 0), while 1 is a perfect score. Scores below 0 indicate worse-than-random performance. We can then aggregate the centered scores for each task, giving us a single number—the PTCORE score—for a base model.&lt;/p&gt;
&lt;h2 id="educational-ablations"&gt;Educational ablations&lt;/h2&gt;
&lt;p&gt;Now that we have a way of measuring how good a model is at European Portuguese, the next step was to run some experiments: How good is the Bagaço v2 dataset for pretraining language models? If we filter data by &lt;a href="https://duarteocarmo.com/blog/bagaco-a-pretraining-dataset-for-european-portuguese.html"&gt;educational score&lt;/a&gt;, do we see a change in the capability of the base model?&lt;/p&gt;
&lt;p&gt;If you remember, every document in the Bagaço v2 dataset has an educational score associated with it:&lt;/p&gt;
&lt;details style="margin:1rem 0"&gt;
  &lt;summary style="cursor:pointer;font-weight:600"&gt;Expand the educational-score rubric&lt;/summary&gt;
  &lt;div class="table-scroll"&gt;
    &lt;table&gt;
      &lt;thead&gt;&lt;tr&gt;&lt;th&gt;Score&lt;/th&gt;&lt;th&gt;Meaning&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
      &lt;tbody&gt;
        &lt;tr&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;No meaningful educational information&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;Some basic educational information, but mostly promotional, irrelevant, or poorly suited to teaching&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;Potentially useful educational material, but superficial, disorganized, or mixed with non-educational content&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;Coherent and appropriate for education, introducing curriculum-relevant concepts, but incomplete or containing some irrelevant material&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;4&lt;/td&gt;&lt;td&gt;Highly relevant, clear, focused, and substantial educational content suitable for structured learning&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;5&lt;/td&gt;&lt;td&gt;Outstanding educational material, perfectly suited to teaching, with detailed reasoning and thorough explanations&lt;/td&gt;&lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;I don't work for a big lab and don't have an H100 GPU sitting under my desk, so I wanted to run an experiment that wouldn't cost thousands of dollars. I trained 12 small language models of around 73 million parameters each:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Depth&lt;/td&gt;
&lt;td style="text-align: right;"&gt;D6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total parameters&lt;/td&gt;
&lt;td style="text-align: right;"&gt;73.53M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Training tokens&lt;/td&gt;
&lt;td style="text-align: right;"&gt;927.99M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seeds&lt;/td&gt;
&lt;td style="text-align: right;"&gt;42 · 1337 · 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filters&lt;/td&gt;
&lt;td style="text-align: right;"&gt;All · ≥1 · ≥2 · ≥3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;About $50 and five hours later, I had the results. For each filter, we get a PTCORE score and a validation BPB (bits per byte, lower is better). PTCORE is shown as a percentage, and each value is the mean ± standard deviation across three seeds. You can also expand the full task-level table to see every individual run.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filter&lt;/th&gt;
&lt;th style="text-align: right;"&gt;PTCORE (%) ↑&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Final validation BPB ↓&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All scores&lt;/td&gt;
&lt;td style="text-align: right;"&gt;10.4 ± 0.9&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;1.0166 ± 0.0016&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Score ≥1&lt;/td&gt;
&lt;td style="text-align: right;"&gt;10.4 ± 0.3&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.0562 ± 0.0009&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Score ≥2&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;12.8 ± 1.3&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.0849 ± 0.0012&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Score ≥3&lt;/td&gt;
&lt;td style="text-align: right;"&gt;11.7 ± 1.2&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1.1059 ± 0.0008&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;details&gt;
  &lt;summary&gt;Expand all D6 task scores&lt;/summary&gt;
  &lt;center&gt;
  &lt;a href="https://duarteocarmo.com/images/108/d6-task-results.webp" target="_blank"&gt;
  &lt;img src="https://duarteocarmo.com/images/108/d6-task-results.webp" alt="PTCORE task scores for all 12 D6 educational-filter runs" style="max-width:100%;border-radius: 2px"&gt;
  &lt;/a&gt;
  &lt;figcaption&gt;All 12 matched D6 runs. Bold marks the best result for each seed and the best mean for each task. Click to expand.&lt;/figcaption&gt;
  &lt;/center&gt;
&lt;/details&gt;

&lt;p&gt;Two interesting findings:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Filtering the pretraining data for documents with higher educational scores improved the base model's performance, but only up to a certain threshold. Filtering by ≥2 performed better than filtering by ≥3.&lt;/li&gt;
&lt;li&gt;The base models trained without filtering (the first row) achieved the lowest validation BPB. This likely reflects how closely the unfiltered training data matches the validation distribution.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The idea that higher-quality pretraining data creates better language models is not groundbreaking. It's shown in research such as &lt;a href="https://arxiv.org/abs/2406.17557"&gt;FineWeb and FineWeb-Edu&lt;/a&gt;, as well as models such as Microsoft's &lt;a href="https://arxiv.org/abs/2306.11644"&gt;Phi&lt;/a&gt;. Still, it's very interesting to see it in practice!&lt;/p&gt;
&lt;p&gt;The best base model will need to balance both PTCORE score and validation BPB (quality AND ability to model language).&lt;/p&gt;
&lt;h2 id="training-larger-and-for-longer"&gt;Training larger and for longer&lt;/h2&gt;
&lt;p&gt;During this work, I trained and tested a lot of small language models on the Bagaço dataset. More than I should have, to be very honest.&lt;/p&gt;
&lt;p&gt;The two highest-scoring runs were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d8_ratio80_ptcore5_education_score_gte2"&gt;&lt;code&gt;ginjinha_d8_ratio80_ptcore5_education_score_gte2&lt;/code&gt;&lt;/a&gt; — PTCORE ≈ 0.163 (126M parameters @ 80 tokens per scaling param)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d11_ratio130_ptcore5_education_score_gte1"&gt;&lt;code&gt;ginjinha_d11_ratio130_ptcore5_education_score_gte1&lt;/code&gt;&lt;/a&gt; — PTCORE ≈ 0.162 (279M parameters @ 130 tokens per scaling param)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's interesting to see a model half the size perform just as well. Filtering the pretraining data can help smaller models close the gap. &lt;a href="https://huggingface.co/duarteocarmo/ginjinha"&gt;For reference, I stored all the Ginjinha runs in this repository&lt;/a&gt;.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/108/wandb-ptcore-metric-evolution.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/108/wandb-ptcore-metric-evolution.webp" alt="Earlier five-task PTCORE metric during pretraining for three Ginjinha runs" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;This is an earlier, pretty unstable PTCORE metric. The 126M-parameter runs have the same training budget.&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;Here are all 22 runs, sorted by PTCORE. Click an available model to open its weights and evaluation files.&lt;/p&gt;
&lt;details style="margin:1rem 0"&gt;
  &lt;summary style="cursor:pointer;font-weight:600"&gt;Expand all Ginjinha runs&lt;/summary&gt;
  &lt;div class="table-scroll"&gt;
    &lt;table&gt;
      &lt;thead&gt;&lt;tr&gt;&lt;th&gt;Tokens&lt;/th&gt;&lt;th&gt;Total parameters&lt;/th&gt;&lt;th&gt;Filter&lt;/th&gt;&lt;th&gt;Model&lt;/th&gt;&lt;th&gt;PTCORE&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
      &lt;tbody&gt;
        &lt;tr&gt;&lt;td&gt;3.355B&lt;/td&gt;&lt;td&gt;125.829M&lt;/td&gt;&lt;td&gt;≥2&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d8_ratio80_ptcore5_education_score_gte2"&gt;&lt;code&gt;D8 · ratio 80&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.163&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;13.393B&lt;/td&gt;&lt;td&gt;279.184M&lt;/td&gt;&lt;td&gt;≥1&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d11_ratio130_ptcore5_education_score_gte1"&gt;&lt;code&gt;D11 · ratio 130&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.162&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;1.678B&lt;/td&gt;&lt;td&gt;125.829M&lt;/td&gt;&lt;td&gt;≥3&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d8_ratio40_ptcore5_education_score_gte3"&gt;&lt;code&gt;D8 · ratio 40&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.154&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;9.787B&lt;/td&gt;&lt;td&gt;279.184M&lt;/td&gt;&lt;td&gt;≥1&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d11_ratio95_ptcore5_education_score_gte1"&gt;&lt;code&gt;D11 · ratio 95&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.150&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;≥2&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_score_gte2_seed42"&gt;&lt;code&gt;D6 · ratio 40 · seed 42&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.140&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;1.678B&lt;/td&gt;&lt;td&gt;125.829M&lt;/td&gt;&lt;td&gt;≥1&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d8_ratio40_ptcore5_education_score_gte1"&gt;&lt;code&gt;D8 · ratio 40&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.138&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;3.355B&lt;/td&gt;&lt;td&gt;125.829M&lt;/td&gt;&lt;td&gt;≥1&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d8_ratio80_ptcore5_education_score_gte1"&gt;&lt;code&gt;D8 · ratio 80&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.137&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;3.355B&lt;/td&gt;&lt;td&gt;125.829M&lt;/td&gt;&lt;td&gt;All&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d8_ratio80_ptcore5_education_all_scores"&gt;&lt;code&gt;D8 · ratio 80&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.133&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;1.678B&lt;/td&gt;&lt;td&gt;125.829M&lt;/td&gt;&lt;td&gt;≥2&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d8_ratio40_ptcore5_education_score_gte2"&gt;&lt;code&gt;D8 · ratio 40&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.133&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;≥2&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_score_gte2_seed1337"&gt;&lt;code&gt;D6 · ratio 40 · seed 1337&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.131&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;≥3&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_score_gte3_seed2026"&gt;&lt;code&gt;D6 · ratio 40 · seed 2026&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.130&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;≥3&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_score_gte3_seed42"&gt;&lt;code&gt;D6 · ratio 40 · seed 42&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.114&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;≥2&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_score_gte2_seed2026"&gt;&lt;code&gt;D6 · ratio 40 · seed 2026&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.114&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;All&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_all_scores_seed42"&gt;&lt;code&gt;D6 · ratio 40 · seed 42&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.110&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;1.678B&lt;/td&gt;&lt;td&gt;125.829M&lt;/td&gt;&lt;td&gt;All&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d8_ratio40_ptcore5_education_all_scores"&gt;&lt;code&gt;D8 · ratio 40&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.110&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;All&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_all_scores_seed1337"&gt;&lt;code&gt;D6 · ratio 40 · seed 1337&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.108&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;≥3&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_score_gte3_seed1337"&gt;&lt;code&gt;D6 · ratio 40 · seed 1337&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.108&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;≥1&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_score_gte1_seed1337"&gt;&lt;code&gt;D6 · ratio 40 · seed 1337&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.107&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;≥1&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_score_gte1_seed2026"&gt;&lt;code&gt;D6 · ratio 40 · seed 2026&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.103&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;≥1&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_score_gte1_seed42"&gt;&lt;code&gt;D6 · ratio 40 · seed 42&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.102&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;927.99M&lt;/td&gt;&lt;td&gt;73.532M&lt;/td&gt;&lt;td&gt;All&lt;/td&gt;&lt;td&gt;&lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d6_ratio40_education_all_scores_seed2026"&gt;&lt;code&gt;D6 · ratio 40 · seed 2026&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;&lt;td&gt;0.093&lt;/td&gt;&lt;/tr&gt;
        &lt;tr&gt;&lt;td&gt;9.375B&lt;/td&gt;&lt;td&gt;279.184M&lt;/td&gt;&lt;td&gt;≥1&lt;/td&gt;&lt;td&gt;&lt;code&gt;D11 · ratio 91&lt;/code&gt;&lt;/td&gt;&lt;td&gt;—&lt;/td&gt;&lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;These models can complete Portuguese text pretty well! Below are two unedited completions from the &lt;a href="https://huggingface.co/duarteocarmo/ginjinha/tree/main/ginjinha_d8_ratio40_ptcore5_education_score_gte3"&gt;D8 ratio-40 model trained on scores ≥3&lt;/a&gt;, generated with a temperature of 0.8 and a top-k of 40:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompt: &lt;code&gt;O bairro de Alvalade&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;O bairro de Alvalade em Lisboa, um bairro de ruas largas e estreitas, onde os lisboetas passam 2/3 do tempo que passa em Lisboa, não tem nada a ver com a sua dimensão.
A partir de um mapa, o percurso que liga Lisboa ao bairro de Alvalade passa por pequenas ruas com traçado irregular e estreitas, com alguns metros de largura, e com pequenas praças a marcarem a malha urbana dos pequenos núcleos urbanos do bairro.
O percurso que liga Alvalade ao bairro de Alvalade passa por pequenas ruas com traçado irregular e estreita, com algumas metros de largura, e com pequenas praças a marcarem a malha urbana dos pequenos núcleos urbanos do bairro.
Há
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Prompt: &lt;code&gt;A cidade do Porto&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;A cidade do Porto possui uma gastronomia única e uma arquitetura única. Desde os pratos de peixe grelhado ao bacalhau guisado e à doçaria conventual. A cidade do Porto oferece uma experiência culinária única, não se afastando muito dos restaurantes, mas convidando a uma boa esplanada enquanto se aprecia o pôr-do-sol.
Para os viajantes que procuram uma experiência gastronómica de excelência, uma viagem ao Porto é indispensável, uma vez que a cidade é muito conhecida pela sua gastronomia rica e requintada. Desde as famosas caves de Vila Nova de Gaia até às famosas caves de vinho do Porto, cada refeição, à refeição principal, é uma oportunidade para conhecer
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Not AGI, but you can see how these models could become quite capable with some SFT and RL on top.&lt;/p&gt;
&lt;h2 id="lessons-learned-and-next-steps"&gt;Lessons learned and next steps&lt;/h2&gt;
&lt;p&gt;This was a fun exercise. We already knew that better-quality data results in better—or at least more capable—models. But running these experiments on a tight budget taught me how structured the research needs to be. I started with a lot of &lt;em&gt;YOLO&lt;/em&gt;-style runs until I had to stop and think: "OK, what do I want to test here?"&lt;/p&gt;
&lt;p&gt;The Ginjinha project also showed me what an enormous advantage labs with access to compute have. If I had a single H100 for a year, I could conduct many more ablations and experiments—and do so &lt;em&gt;much&lt;/em&gt; faster. Perhaps I should invest in one.&lt;/p&gt;
&lt;p&gt;And even though I haven't trained on the entire Bagaço v2 dataset, I did realize that it has a major shortcoming: data quality. Approximately &lt;a href="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco2#statistics--counts"&gt;50% of the documents in Bagaço v2 have an educational score of 0 (i.e., they have zero educational value)&lt;/a&gt;. Yes, you read that right. That's not going to get us where we need to be. It might add some diversity, but we need much more high-quality data.&lt;/p&gt;
&lt;p&gt;Yes. I'm working on it!&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Retrospectiva #9</title><link href="https://duarteocarmo.com/blog/retrospectiva-9.html" rel="alternate"/><published>2026-07-06T00:00:00+02:00</published><updated>2026-07-06T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-07-06:/blog/retrospectiva-9.html</id><summary type="html">&lt;p&gt;This month's Retrospectiva comes a little later than usual. The whole house got sick battling the flu the past couple weeks. Mixed with a small heatwave here in Copenhagen, means summer hasn't blessed us like we expected. At least not yet.&lt;/p&gt;
&lt;p&gt;Now that I'm feeling a bit better, I finally …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This month's Retrospectiva comes a little later than usual. The whole house got sick battling the flu the past couple weeks. Mixed with a small heatwave here in Copenhagen, means summer hasn't blessed us like we expected. At least not yet.&lt;/p&gt;
&lt;p&gt;Now that I'm feeling a bit better, I finally managed to get some writing in. Let's get to it.&lt;/p&gt;
&lt;h2 id="using"&gt;Using&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://huggingface.co/zai-org/GLM-5.2" target="_blank"&gt;GLM-5.2&lt;/a&gt;&lt;/strong&gt;: Finally. An open-weights model that competes with the big frontier models. For the past weeks I've been using GLM-5.2 extensively. Through &lt;a href="https://opencode.ai/go"&gt;OpenCode Go&lt;/a&gt; it starts at 5 USD/month. It's different than using your GPT 5.5 or Fable, but it feels good in the open-source way. I urge you to take it for a spin. It costs about one-fourth of what Fable costs. And it deals with 99% of issues Fable does.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/107/glm-52.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/107/glm-52.webp" alt="Artificial Analysis chart showing GLM-5.2 cost per intelligence index task" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
GLM-5.2 catching up - &lt;a href="https://artificialanalysis.ai/" target="_blank"&gt;Artificial Analysis&lt;/a&gt;
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/phun333/pi-infobar" target="_blank"&gt;Pi Stats&lt;/a&gt;&lt;/strong&gt;: You probably know by now that I'm a big fan of the &lt;a href="https://pi.dev/"&gt;Pi&lt;/a&gt; harness. If you like Claude Code better, that's fine - enjoy yourself. One issue with Pi is that it's hard to know exactly how much you're spending. I discovered this little application called &lt;a href="https://github.com/phun333/pi-infobar"&gt;Pi Stats&lt;/a&gt;, which gives me a widget in my menu bar that shows me exactly how much I've spent and on what. Even though I still pay for a fixed plan every month, it's still important to know (in terms of real value) what's going where.&lt;/p&gt;
&lt;figure style="margin: 0;"&gt;
&lt;div style="display:flex;gap:1rem;justify-content:center;align-items:flex-start;flex-wrap:wrap;"&gt;
&lt;a href="https://duarteocarmo.com/images/107/pistats-overview.webp" target="_blank" style="flex:1 1 260px;max-width:45%;text-decoration:none;"&gt;
&lt;img src="https://duarteocarmo.com/images/107/pistats-overview.webp" alt="Pi Stats overview screen" style="width:100%;max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;a href="https://duarteocarmo.com/images/107/pistats-languages.webp" target="_blank" style="flex:1 1 260px;max-width:45%;text-decoration:none;"&gt;
&lt;img src="https://duarteocarmo.com/images/107/pistats-languages.webp" alt="Pi Stats languages screen" style="width:100%;max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/div&gt;
&lt;figcaption&gt;
Pi Stats in action (not my usage)
&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/jesseduffield/lazygit" target="_blank"&gt;Lazygit&lt;/a&gt;&lt;/strong&gt;: We write less code, but we are accountable for more. Some of it is not worth looking at — but the important bits are. For the past few months I've experimented with a lot of "code diff viewers". I need somewhere I can review the changes the agent has made, and direct it in case anything goes wrong or weird. I've tried &lt;a href="https://github.com/modem-dev/hunk"&gt;Hunk&lt;/a&gt; (and even contributed a couple of PRs: &lt;a href="https://github.com/modem-dev/hunk/pull/310"&gt;#310&lt;/a&gt;, &lt;a href="https://github.com/modem-dev/hunk/pull/347"&gt;#347&lt;/a&gt;), and &lt;a href="https://github.com/esmuellert/codediff.nvim"&gt;codediff.nvim&lt;/a&gt; in Neovim, which is what my &lt;code&gt;&amp;lt;space&amp;gt;d+d&lt;/code&gt; mapping calls. But lazygit paired with &lt;a href="https://github.com/Wilfred/difftastic"&gt;difftastic&lt;/a&gt; has been hard to beat. Fast to iterate and great diff highlighting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.sofascore.com/" target="_blank"&gt;Sofascore&lt;/a&gt;&lt;/strong&gt;: I like watching sports. Not all sports, not all the time. But when I do (surprise), I like looking at the data. Sofascore is the app that gives me all of this - in a way I love. Who plays where? Who used to play where? When you're not 15 and playing FIFA extensively - it's hard to keep up. This team out of Croatia has built an incredible app!&lt;/p&gt;
&lt;h2 id="reading"&gt;Reading&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.oreilly.com/library/view/deep-learning-for/9781098168025/" target="_blank"&gt;Deep Learning for Biology&lt;/a&gt;&lt;/strong&gt;: I finished this one over 2-3 weeks. I recommend it. A great read in an area where there's much left to do!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://medarc-ai.github.io/mindeye/" target="_blank"&gt;Reconstructing the Mind's Eye&lt;/a&gt;&lt;/strong&gt;: Some wild research out of Princeton that shows how they reconstruct images from brain activity. Yes. You heard that right. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://lilianweng.github.io/posts/2026-06-24-scaling-laws/" target="_blank"&gt;Scaling Laws, Carefully - Lilian Weng&lt;/a&gt;&lt;/strong&gt;: What is the ideal amount of data given a certain model size? Given a certain compute budget? And vice versa? These are just some of the questions researchers have been asking themselves. Lilian writes about them beautifully.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/107/chinchilla-2.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/107/chinchilla-2.webp" alt="Chinchilla scaling laws comparison" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Image source: &lt;a href="https://arxiv.org/abs/2203.15556" target="_blank"&gt;Hoffmann et al. 2022&lt;/a&gt;, as shown in Lilian Weng's &lt;a href="https://lilianweng.github.io/posts/2026-06-24-scaling-laws/" target="_blank"&gt;Scaling Laws, Carefully&lt;/a&gt;.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://injuly.in/blog/napkin-inference-cost/index.html" target="_blank"&gt;Inference cost at scale with napkin math&lt;/a&gt;&lt;/strong&gt;: A fast and short reference on how to think about serving models on GPUs. If you wanted to serve GLM-5.2 to multiple users, what would that cost?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://lonriesberg.com/posts/investing-with-agents/" target="_blank"&gt;Investing with Agents - Lon Riesberg&lt;/a&gt;&lt;/strong&gt;: A very fun read from Lon Riesberg (creator of the awesome &lt;a href="https://dataelixir.com/"&gt;Data Elixir newsletter&lt;/a&gt;) about using LLMs to make investments in the stock market. &lt;/p&gt;
&lt;h2 id="listening"&gt;Listening&lt;/h2&gt;
&lt;p&gt;Not the most inspiring month musically. Being sick means less running, which means less listening.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.mlst.ai/" target="_blank"&gt;Machine Learning Street Talk&lt;/a&gt;&lt;/strong&gt;: One of my favourite podcasts about Machine Learning/Deep Learning/AI. You should give it a listen.&lt;/p&gt;
&lt;iframe data-testid="embed-iframe" style="border-radius:12px" src="https://open.spotify.com/embed/show/02e6PZeIOdpmBGT9THuzwR?utm_source=generator&amp;theme=0" width="100%" height="152" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"&gt;&lt;/iframe&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://soundcloud.com/worldwide-fm/skate-muzik-listen-to-sade-14" target="_blank"&gt;Skate Muzik: Listen to SADE - Worldwide FM&lt;/a&gt;&lt;/strong&gt;: I mean - Sade. Do I need to expand?&lt;/p&gt;
&lt;iframe width="100%" height="166" scrolling="no" frameborder="no" allow="autoplay; encrypted-media" src="https://w.soundcloud.com/player/?visual=false&amp;url=https%3A%2F%2Fapi.soundcloud.com%2Ftracks%2F2323007471&amp;show_artwork=true"&gt;&lt;/iframe&gt;

&lt;h2 id="watching"&gt;Watching&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.sofascore.com/football/tournament/world/world-championship/16#id:58210" target="_blank"&gt;World Cup 2026&lt;/a&gt;&lt;/strong&gt;: Norway just eliminated Brazil. Morocco eliminated Canada. Portugal drew with Congo and is playing Spain today. I'm not a sports fanatic, but I come from a place where football is very close to religion. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.wser.org/" target="_blank"&gt;Western States&lt;/a&gt;&lt;/strong&gt;: When I can't run, I watch other people run. This year's Western States 100 miler was an exciting one to watch. Even though I was rooting for &lt;a href="https://utmb.world/runner/5883300.hans.troyer"&gt;Hans Troyer&lt;/a&gt; to win this one, &lt;a href="https://www.wser.org/2026/06/29/2026-race-recap/"&gt;Vincent Bouillard&lt;/a&gt; made an amazing run! Record broken, again! &lt;/p&gt;</content><category term="blog"/></entry><entry><title>How I Hermes</title><link href="https://duarteocarmo.com/blog/how-i-hermes.html" rel="alternate"/><published>2026-06-13T00:00:00+02:00</published><updated>2026-06-13T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-06-13:/blog/how-i-hermes.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/106/banner.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/106/banner.webp" alt="How I Hermes" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;In January this year, &lt;a href="https://openclaw.ai/"&gt;OpenClaw&lt;/a&gt; went viral. You probably heard about it. My guess is some of you even use it - or at least tried to. I certainly did. &lt;/p&gt;
&lt;p&gt;I remember it clearly. I installed it, and very quickly noticed I was now spending time debugging a &lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/features/cron/"&gt;cron job&lt;/a&gt; created …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/106/banner.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/106/banner.webp" alt="How I Hermes" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;In January this year, &lt;a href="https://openclaw.ai/"&gt;OpenClaw&lt;/a&gt; went viral. You probably heard about it. My guess is some of you even use it - or at least tried to. I certainly did. &lt;/p&gt;
&lt;p&gt;I remember it clearly. I installed it, and very quickly noticed I was now spending time debugging a &lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/features/cron/"&gt;cron job&lt;/a&gt; created by an AI agent - in what looked like a pretty messy code base. Quickly deleted it after that episode.&lt;/p&gt;
&lt;p&gt;A couple months later I decided to download &lt;a href="https://hermes-agent.nousresearch.com/"&gt;Hermes Agent&lt;/a&gt;. I must admit, I've been using it pretty much every day since then. I was hooked. Let's talk about it.&lt;/p&gt;
&lt;h2 id="what-the-hermes-agent-is-and-isnt"&gt;What the Hermes Agent is, and isn't&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://nousresearch.com/"&gt;Nous Research&lt;/a&gt; is a research lab that first got popular by building uncensored models. After the OpenClaw boom, they launched their own version of it: &lt;a href="https://hermes-agent.nousresearch.com/"&gt;Hermes Agent&lt;/a&gt;. An always-on agent that runs on your computer and can act as your personal assistant. It's like a ChatGPT that has 24/7 access to your computer, can read/edit files, can browse the web, can write code, etc. You talk to it via the terminal, a web interface, messaging apps (Telegram, WhatsApp), or even a &lt;a href="https://hermes-agent.nousresearch.com/desktop"&gt;desktop app&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;Once you install it, you configure the agent by creating (or installing) &lt;a href="https://agentskills.io/home"&gt;skills&lt;/a&gt;. More on that below.&lt;/p&gt;
&lt;p&gt;Hermes is not a plug-and-play experience. Do not expect to just install it and be amazed. You can even follow a &lt;a href="https://www.youtube.com/watch?v=1ve4Atbqmoo"&gt;tutorial&lt;/a&gt; and still not get much out of it. You need to make it your own. You need to spend a considerable amount of time doing it. It reminds me a bit of Vim.   &lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/106/cover-mockups.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/106/cover-mockups.webp" alt="Saramago running as my Hermes Agent in Telegram" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Saramago, my Hermes agent. Reminding me to take out the garbage, and monitoring my GPU-bound jobs.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;h2 id="my-agent-setup-saramago"&gt;My agent setup: Saramago&lt;/h2&gt;
&lt;p&gt;There's a pretty recent &lt;a href="https://www.businessinsider.com/coders-keep-laptops-open-in-public-ai-agent-2026-5"&gt;meme of programmers leaving their laptops open.&lt;/a&gt;. I don't like to keep my computer open in random places. I'm also not particularly interested in having an agent running freely inside my main machine. &lt;/p&gt;
&lt;p&gt;Fortunately, for the past 5 years, I've been running most of my remote computing on a &lt;a href="https://duarteocarmo.com/blog/how-i-self-host-in-2024.html"&gt;refurbished Hetzner server&lt;/a&gt;. For a fixed monthly price, I have my own Debian machine that is available 24/7. This is where Saramago (yes, I gave him a name) runs. &lt;/p&gt;
&lt;p&gt;To keep things simple and reliable - I talk to Saramago via Telegram. I've heard WhatsApp is a bit flaky. And streaming message support in Telegram is great!&lt;/p&gt;
&lt;p&gt;Right now, Saramago runs primarily on three models. DeepSeek V4 (flash or pro depending on task complexity) - through the &lt;a href="https://api-docs.deepseek.com/quick_start/pricing"&gt;official DeepSeek API&lt;/a&gt;, which is dirt cheap. For the most important things - I run GPT 5.5 via my Codex subscription. I tend to experiment every now and then with new models and see how they perform. I would also love to tell you the story of how I use a fully local, private, open-source model, but we're not there yet.&lt;/p&gt;
&lt;p&gt;Enough babbling. What about use cases?&lt;/p&gt;
&lt;h2 id="use-case-1-coding-on-the-go"&gt;Use case 1: Coding on the go&lt;/h2&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/106/code-mockups.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/106/code-mockups.webp" alt="Saramago helping with coding tasks from Telegram" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Creating PRs and iterating on the go.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;The first use case is the most obvious one. Having Hermes is a bit like having a [insert your favourite coding agent here] that is available 24/7 on the go. I don't use it when I need to do especially &lt;em&gt;deep&lt;/em&gt; work. But it's very nice for small annoyances while I am on the go. It has access to the &lt;a href="https://cli.github.com/"&gt;GitHub CLI&lt;/a&gt; and a GitHub CLI skill so it can interact with any repo. So whenever I want to fix a broken pipeline, a typo on this blog, a small fix - it just creates a PR and sends me a link for review.&lt;/p&gt;
&lt;h2 id="use-case-2-setting-dynamicreactive-reminders"&gt;Use case 2: Setting dynamic/reactive reminders&lt;/h2&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/106/reminders-mockups.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/106/reminders-mockups.webp" alt="Saramago handling conditional reminders in Telegram" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Creating conditional reminders on the go.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;I'm a big &lt;a href="https://ticktick.com/"&gt;TickTick&lt;/a&gt; user. It's my go-to todo app. It runs my life. But there is a certain type of reminder that TickTick is just not able to do. &lt;/p&gt;
&lt;p&gt;Some examples: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"Remind me to pack up my rain jacket if it rains outside tomorrow"&lt;/li&gt;
&lt;li&gt;"Remind me which type of trash to bring outside for pickup tomorrow" - This is complicated, &lt;a href="https://www.atarifiuti.an.it/comuni.php?idcomune=36&amp;amp;pag=calendario"&gt;especially in Italy&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;“Remind me to email X if I don’t get that email in my inbox”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hermes also knows when to stay silent. If it doesn't rain tomorrow it just won't text me, instead of texting me - "Hey - no rain tomorrow". Which is just annoying.&lt;/p&gt;
&lt;h2 id="use-case-3-monitoring-long-running-jobs"&gt;Use case 3: Monitoring long-running jobs&lt;/h2&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/106/monitoring-mockups.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/106/monitoring-mockups.webp" alt="Saramago monitoring long-running jobs from Telegram" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
No terminal babysitting. Saramago does it for me.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;More often than not, I run long-running jobs on remote computers. Sometimes it's a large GPU training run. Sometimes it's a long-running data parsing or enrichment job. Can be anything really. &lt;/p&gt;
&lt;p&gt;I used to have to click a browser, ssh into a server, leave tmux open, and see if anything broke. Now - Saramago can do it for me. "SSH into the machine X and give me an update on the running job every 15 mins - turn off the machine when done". By giving access to CLIs/SDKs, like the &lt;a href="https://vast.ai/developers/cli"&gt;vast.ai CLI&lt;/a&gt;, the whole loop is automated.&lt;/p&gt;
&lt;h2 id="use-case-4-managing-my-calendar-for-me"&gt;Use case 4: Managing my calendar for me&lt;/h2&gt;
&lt;p&gt;I'm not particularly proud of it. But most of my email runs on Gmail. Saramago has access to the &lt;a href="https://github.com/googleworkspace/cli"&gt;Google Workspace CLI&lt;/a&gt;. It can read my email, check my calendar - and interact with both. &lt;/p&gt;
&lt;p&gt;This unlocks yet another space for convenient automations. "Check email for the last train trip and add it to my calendar". "Every time a new email comes in - if it mentions a work location - add an all-day event to my calendar". Most of the useful (and not dangerous) things here come from the agent connecting your email to your calendar better than Google does. &lt;/p&gt;
&lt;h2 id="use-case-5-choosing-flights"&gt;Use case 5: Choosing flights&lt;/h2&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/106/flights-mockups.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/106/flights-mockups.webp" alt="Saramago comparing flight options in Telegram" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
The future of flight search: Matplotlib.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;"An agent that books flights for you" is the dream that has been sold to us for many years. An LLM taking pictures of a browser and trying to click around the Google Flights interface has never worked. &lt;/p&gt;
&lt;p&gt;But not anymore. Saramago has access to the &lt;a href="https://github.com/punitarani/fli"&gt;Google Flights CLI&lt;/a&gt;. That means I can search flights on my own terms. I can be as annoying as I want with my peculiar travelling choices. And plot them just the way I like. When it's time to book - I will do it. Much better than any Momondo-like experience.&lt;/p&gt;
&lt;h2 id="use-case-6-writing-and-brainstorming"&gt;Use case 6: Writing and brainstorming&lt;/h2&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/106/writing-mockups.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/106/writing-mockups.webp" alt="Saramago helping with writing and brainstorming in Telegram" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Getting the most done before I sit at the desk.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;With a kid, I have less and less time in front of the actual computer. Thankfully, I have Saramago. That means that I can do a lot of the prep work on the go, and once I sit down - I can do what matters. &lt;/p&gt;
&lt;p&gt;Creating the structure for my blog post, adding the images and figcaptions, the boilerplate, etc. The agent can do that. And I can focus on the writing. Massive productivity booster. With the &lt;a href="https://core.telegram.org/api/transcribe"&gt;Telegram API&lt;/a&gt;, I can also send a voice message to Saramago, and it will understand. Telegram will transcribe automatically.&lt;/p&gt;
&lt;h2 id="use-case-7-fitness-coaching"&gt;Use case 7: Fitness coaching&lt;/h2&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/106/fitness-mockups.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/106/fitness-mockups.webp" alt="Saramago giving fitness coaching feedback in Telegram" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
It's getting closer to being my fitness coach. But not there yet.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;I like running. I have both a Garmin and an Oura ring. I've been on a quest to make Saramago my running coach. Giving me feedback after a run, preparing my next run, managing my running volume - you see what I mean. I think there's something interesting here - in connecting all the different sources of data and delivering them to me with a small text. The best success I've had was using a &lt;a href="https://github.com/duarteocarmo/skills/tree/main/skills"&gt;custom skill I built&lt;/a&gt; + the strava-cli. But I'm still not 100% satisfied. There's a good use case there somewhere - but I'm not there yet.&lt;/p&gt;
&lt;h2 id="why-hermes-and-not-something-else"&gt;Why Hermes, and not something else?&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://openclaw.ai/"&gt;OpenClaw&lt;/a&gt;, &lt;a href="https://github.com/NVIDIA/NemoClaw"&gt;NemoClaw&lt;/a&gt;, &lt;a href="https://nanoclaw.dev/"&gt;NanoClaw&lt;/a&gt;, &lt;a href="https://github.com/HKUDS/nanobot"&gt;Nanobot&lt;/a&gt;, &lt;a href="https://www.ironclaw.com/"&gt;IronClaw&lt;/a&gt;. There are a lot of alternatives out there. So why Hermes? I don't know. It's a bit like asking about tabs vs. spaces or Vim vs. Emacs. I tried 3-4 and nothing really stuck with me - except for Hermes. I didn't notice any feature impairing bugs - which was a good start. &lt;/p&gt;
&lt;p&gt;If I had to mention two things: The first is the robust &lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/features/cron/"&gt;cron jobs + reminders setup&lt;/a&gt;. The agent automatically creates jobs, re-runs them at a schedule, knows when NOT to ping you. The second is the &lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/features/skills#agent-managed-skills-skill_manage-tool"&gt;automated skill-creation&lt;/a&gt;. When the agent does something "complex" it creates a skill for it. Next time it needs to do it - it loads that same skill. It works really well. &lt;/p&gt;
&lt;h2 id="theres-something-great-here"&gt;There's something great here.&lt;/h2&gt;
&lt;p&gt;Some say that tools like OpenClaw and Hermes are gigantic piles of vibe-coded slop. And I agree - some of it definitely is. There are bugs here and there. But they're getting more and more fixed. And I like Hermes' &lt;a href="https://github.com/NousResearch/hermes-agent/blob/main/AGENTS.md#contribution-rubric--what-we-want--what-we-dont"&gt;Philosophy&lt;/a&gt; on this. &lt;/p&gt;
&lt;p&gt;But these products are also deeply interesting. They are a tinkerer's dream. With enough time and care, you can really transform them into something insanely useful. It takes time. It takes patience, and a good amount of technical expertise. It reminds me of spending time Jailbreaking my iPhone, or learning about Vim. It's fun! &lt;/p&gt;
&lt;p&gt;Not only fun - these tools are also a productivity boost - a small glance into the future. Even the big boys have noticed. &lt;a href="https://gemini.google/overview/agent/spark/"&gt;Google's Gemini Spark&lt;/a&gt; is a good example. &lt;/p&gt;
&lt;p&gt;Let's hope they don't ruin it for the rest of us. &lt;sup id="sf-how-i-hermes-1-back"&gt;&lt;a href="#sf-how-i-hermes-1" class="simple-footnote" title="I would feel bad if I didn't include a sentence or two about security. So here it is. None of these personal assistants are perfect. They are all vulnerable to the lethal trifecta. Act accordingly."&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-how-i-hermes-1"&gt;I would feel bad if I didn't include a sentence or two about security. So here it is. None of these personal assistants are perfect. They are all vulnerable to the &lt;a href="https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/"&gt;lethal trifecta&lt;/a&gt;. Act &lt;a href="https://hermes-agent.nousresearch.com/docs/user-guide/security/"&gt;accordingly&lt;/a&gt;. &lt;a href="#sf-how-i-hermes-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>Retrospectiva #8</title><link href="https://duarteocarmo.com/blog/retrospectiva-8.html" rel="alternate"/><published>2026-06-04T00:00:00+02:00</published><updated>2026-06-04T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-06-04:/blog/retrospectiva-8.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/105/italy-allegra.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/105/italy-allegra.webp" alt="Walking with Allegra in the Italian countryside" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;If there's one thing Retrospectiva does for me - other than giving me an excuse to write for you of course - it's making me realise how fast time moves.&lt;/p&gt;
&lt;p&gt;It's June already - and May was – well – hectic to say the least.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.datamakersfest.com/"&gt;Data Makers Fest&lt;/a&gt; was awesome - and I absolutely love Porto …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/105/italy-allegra.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/105/italy-allegra.webp" alt="Walking with Allegra in the Italian countryside" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;If there's one thing Retrospectiva does for me - other than giving me an excuse to write for you of course - it's making me realise how fast time moves.&lt;/p&gt;
&lt;p&gt;It's June already - and May was – well – hectic to say the least.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.datamakersfest.com/"&gt;Data Makers Fest&lt;/a&gt; was awesome - and I absolutely love Porto. The &lt;a href="https://duarteocarmo.com/pdfs/the_great_cleanup_compressed.pdf"&gt;talk&lt;/a&gt; resonated. Many had questions after the talk and – as usual – I didn't have any answers.&lt;/p&gt;
&lt;p&gt;A week after that, while out on a Saturday morning run, I saw some runners with Copenhagen Marathon goodie bags. I got so jealous. A marathon? In my own city? And I'm NOT running? "What are you waiting for?" Vitto asked. "At least go for a PB - you have nothing to lose." I signed up for a last-minute ticket, and the rest is history.&lt;/p&gt;
&lt;p&gt;We then came to Italy and spent the last half of the month putting things in order here - making sure we feel at home, and making sure Allegra is as happy as she can be.&lt;/p&gt;
&lt;p&gt;So yeah - the month felt hectic.&lt;/p&gt;
&lt;h2 id="using"&gt;Using&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://artificialanalysis.ai/models/deepseek-v4-pro" target="_blank"&gt;DeepSeek V4 Pro&lt;/a&gt;&lt;/strong&gt;: I've &lt;a href="/blog/retrospectiva-7.html"&gt;talked about Hermes before&lt;/a&gt;. And I know I still owe a post about how I use Hermes on a daily basis. Agent harnesses (like &lt;a href="https://openclaw.ai/"&gt;OpenClaw&lt;/a&gt;) can become real token hoarders. Truth is, over the past few weeks, I've barely felt the cost. DeepSeek V4 Pro is not only an incredibly smart model, but also &lt;em&gt;insanely&lt;/em&gt; cheap. If that wasn't enough, what I thought was a &lt;em&gt;momentary&lt;/em&gt; discount &lt;a href="https://www.reddit.com/r/ArtificialInteligence/comments/1tlgw8d/deepseek_just_confirmed_that_their_75_promo/"&gt;became the official price&lt;/a&gt;. I always use it through the official DeepSeek API. It has some quirks (like &lt;a href="https://api-docs.deepseek.com/quick_start/rate_limit#request-keep-alive-mechanism"&gt;keeping requests open while they wait to be scheduled&lt;/a&gt;), but it has been reliable and incredibly cheap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://vast.ai/" target="_blank"&gt;Vast.ai&lt;/a&gt;&lt;/strong&gt;: Remember when I wrote about Scaleway in &lt;a href="/blog/retrospectiva-7.html"&gt;last month's Retrospectiva&lt;/a&gt;? Scaleway is awesome. But sometimes I want compute at a dirt-cheap price. Sometimes, I need to run a 27B model on a reliable A100 at 1.5 USD/hour. That's what Vast does for me. Also, &lt;a href="https://vast.ai/developers/cli"&gt;their CLI&lt;/a&gt; is pretty good, I just set it up, and my agent takes care of the rest.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://runrepeat.com/hoka-hopara-2" target="_blank"&gt;HOKA Hopara 2&lt;/a&gt;&lt;/strong&gt;: Are these shoes incredibly ugly? Yes. Are they the sort of thing you probably don't want to wear out in public? Yes. But for someone who loves to walk freely, run, hike, and pack light - these have been incredible. I use them with socks most of the time. They can get wet, and they dry incredibly fast. If you don't care what other people think (hint: you shouldn't), and want shoes that you can wear to most activities in the summer, consider them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://huggingface.co/collections/google/translategemma" target="_blank"&gt;TranslateGemma&lt;/a&gt;&lt;/strong&gt;: You probably know by now that I'm on a quest to build European Portuguese large language models. I feel like this model release is under-hyped. It's open source, and it supports underrepresented languages like Catalan, European Portuguese, &lt;a href="https://huggingface.co/google/translategemma-4b-it/resolve/main/chat_template.jinja"&gt;and others&lt;/a&gt;. It's available in 3 different sizes. To give you some context, when translating English to Portuguese, the 4B TranslateGemma model &lt;strong&gt;beats&lt;/strong&gt; Gemma 3 27B. The &lt;a href="https://arxiv.org/pdf/2601.09012"&gt;technical report&lt;/a&gt; is worth reading.&lt;/p&gt;
&lt;h2 id="reading"&gt;Reading&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://newsletter.maartengrootendorst.com/p/a-visual-guide-to-gemma-4-12b" target="_blank"&gt;A Visual Guide to Gemma 4 12B - Maarten Grootendorst&lt;/a&gt;&lt;/strong&gt;: Local models are becoming increasingly viable. Especially the 4 to 27B range. This is a nice deep dive into one of the latest models from DeepMind. Instead of using an image or audio encoder, the model uses a lightweight embedding layer. A great write-up from Maarten. Worth the read.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/105/gemma-4-encoder-free.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/105/gemma-4-encoder-free.webp" alt="Gemma 4 encoder-free diagram" style="max-width:75%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Image from Maarten Grootendorst's &lt;a href="https://newsletter.maartengrootendorst.com/p/a-visual-guide-to-gemma-4-12b" target="_blank"&gt;A Visual Guide to Gemma 4 12B&lt;/a&gt;.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://microsoft.ai/wp-content/uploads/2026/06/main_20260602_2.pdf" target="_blank"&gt;MAI-Thinking-1: Building a Hill-Climbing Machine - Microsoft AI&lt;/a&gt;&lt;/strong&gt;: Microsoft &lt;a href="https://microsoft.ai/news/introducing-mai-thinking-1/"&gt;announced&lt;/a&gt; a nice set of &lt;a href="https://microsoft.ai/models/"&gt;new models&lt;/a&gt;. Among them is MAI-Thinking-1, a new reasoning foundation model. This technical report is a gold mine. Unlike many, it answers the &lt;em&gt;why&lt;/em&gt;, rather than only the what.&lt;/p&gt;
&lt;p&gt;This was a month where I scanned many books, but had a hard time sticking to one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.goodreads.com/book/show/231191100-ai-systems-performance-engineering" target="_blank"&gt;AI Systems Performance Engineering&lt;/a&gt;&lt;/strong&gt;: I started this one and read about 2-3 chapters. Too dense (which is not something I usually say). I felt like reading a CUDA documentation book. It &lt;em&gt;could&lt;/em&gt; be interesting. But this one didn't really convince me.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.goodreads.com/book/show/223853703-deep-learning-for-biology" target="_blank"&gt;Deep Learning for Biology&lt;/a&gt;&lt;/strong&gt;: I'm around 50% done with this one and I can already say that it is a great read. I've worked on a few biology-related projects, but I was always curious to learn more about the field. This book delivers. Well written, practical, and tangible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.goodreads.com/book/show/58988399-raising-raffi" target="_blank"&gt;Raising Raffi - Keith Gessen&lt;/a&gt;&lt;/strong&gt;: Every single parenting book seems to focus on what you could (or should) be doing better. I'm a bit sick of that approach. This book is the first real, honest, and raw tale from the perspective of a new father. It's chaos! You are not ready. And that's okay - nobody is. It's the first parenting book that resonated with me. And I haven't even finished it.&lt;/p&gt;
&lt;h2 id="listening"&gt;Listening&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://open.spotify.com/track/6tnqBTSp8YM622LKgdvxSJ" target="_blank"&gt;Eyes Off You - Tokyo Tea Room&lt;/a&gt;&lt;/strong&gt;: My favourite band of the past 3 years has a new single. That single is now on repeat.&lt;/p&gt;
&lt;iframe data-testid="embed-iframe" style="border-radius:12px" src="https://open.spotify.com/embed/track/6tnqBTSp8YM622LKgdvxSJ?utm_source=generator&amp;theme=0" width="100%" height="152" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"&gt;&lt;/iframe&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://open.spotify.com/artist/2aGCFk3Mvhs9Am7JHwoyRr" target="_blank"&gt;Svaneborg Kardyb&lt;/a&gt;&lt;/strong&gt;: May was definitely a Nordic Jazz month for me. Svaneborg Kardyb is a duo from Denmark that makes beautifully simple music. You can feel where their Superkilen album takes its name from. I recommend getting lost in the discography.&lt;/p&gt;
&lt;iframe data-testid="embed-iframe" style="border-radius:12px" src="https://open.spotify.com/embed/track/2UDn2Wim426pQUy0j1LgBr?utm_source=generator&amp;theme=0" width="100%" height="152" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"&gt;&lt;/iframe&gt;

&lt;h2 id="watching"&gt;Watching&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.rolandgarros.com/en-us/" target="_blank"&gt;Roland-Garros 2026&lt;/a&gt;&lt;/strong&gt;: Vitto and I love to watch tennis. The top three players in the world are out, and we will have a new Grand Slam winner. There are still two Italians in the draw, and they will play each other tomorrow. I think that's what I love about tennis. It's always a bit like an adventure - you never know what will happen.
&lt;br&gt;
&lt;br&gt;
&lt;lite-youtube videoid="r4Osxyp0fTY"&gt;&lt;/lite-youtube&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.youtube.com/watch?v=r4Osxyp0fTY" target="_blank"&gt;A Beginner's Guide To Ryuichi Sakamoto - Derrick Gee&lt;/a&gt;&lt;/strong&gt;: For years, I heard my dad blast Sakamoto while studying for long stretches over the weekend. Dad always had good taste in music. It's been 3 years since we lost Ryuichi to cancer. His music, though, is timeless.&lt;/p&gt;
&lt;p&gt;Let's hope June is less hectic. Enjoy the summer weather!&lt;/p&gt;</content><category term="blog"/></entry><entry><title>TTS doesn't suck anymore</title><link href="https://duarteocarmo.com/blog/tts-doesnt-suck-anymore.html" rel="alternate"/><published>2026-05-26T00:00:00+02:00</published><updated>2026-05-26T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-05-26:/blog/tts-doesnt-suck-anymore.html</id><summary type="html">&lt;p&gt;About 6 months ago I wrote a small rant on how &lt;a href="https://duarteocarmo.com/blog/tts-still-sucks.html"&gt;open source TTS models still sucked&lt;/a&gt;. 6 months later, I'm happy to report that isn't the case anymore.&lt;/p&gt;
&lt;p&gt;January this year, &lt;a href="https://qwen.ai/home"&gt;Qwen&lt;/a&gt;, the famous Chinese AI lab, released &lt;a href="https://github.com/QwenLM/Qwen3-TTS"&gt;Qwen3-TTS&lt;/a&gt;, an open-weights series of TTS models. The release included 2 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;About 6 months ago I wrote a small rant on how &lt;a href="https://duarteocarmo.com/blog/tts-still-sucks.html"&gt;open source TTS models still sucked&lt;/a&gt;. 6 months later, I'm happy to report that isn't the case anymore.&lt;/p&gt;
&lt;p&gt;January this year, &lt;a href="https://qwen.ai/home"&gt;Qwen&lt;/a&gt;, the famous Chinese AI lab, released &lt;a href="https://github.com/QwenLM/Qwen3-TTS"&gt;Qwen3-TTS&lt;/a&gt;, an open-weights series of TTS models. The release included 2 CustomVoice models (pre-made voices + style control), 2 base models (zero-shot voice cloning + fine-tuning), and a VoiceDesign model (create voices from descriptions). With a 0.6B and a 1.7B variant - they're all quite small.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/104/qwen3-tts-introduction.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/104/qwen3-tts-introduction.webp" alt="Qwen3-TTS introduction" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Qwen3-TTS in a nutshell. &lt;a href="https://github.com/QwenLM/Qwen3-TTS"&gt;credits&lt;/a&gt;.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;There are a lot of things to like. First, it &lt;em&gt;fully&lt;/em&gt; supports voice cloning - via fine-tuning or zero-shot conditioning. &lt;a href="https://mistral.ai/news/voxtral"&gt;Voxtral&lt;/a&gt;, for example, &lt;a href="https://www.reddit.com/r/LocalLLaMA/comments/1s6rmoi/the_missing_piece_of_voxtral_tts_to_enable_voice/"&gt;doesn't&lt;/a&gt;. Second: the license is Apache 2.0 - which means we can do whatever we want with it (&lt;a href="https://github.com/fishaudio/fish-speech/blob/main/LICENSE"&gt;beware&lt;/a&gt;). Third, it's supported by a strong inference engine. In this case &lt;a href="https://docs.vllm.ai/projects/vllm-omni/en/latest/"&gt;vLLM-Omni&lt;/a&gt;. And more importantly: it avoids many of the small issues other open-source TTS models had when generating longer pieces of text — squeaks, audio drops, weird pacing, etc.&lt;/p&gt;
&lt;p&gt;There are some caveats. There is a &lt;a href="https://github.com/QwenLM/Qwen3-TTS/pull/178"&gt;small bug&lt;/a&gt; on the fine-tuning code which creates some weird accelerations. The &lt;a href="https://github.com/QwenLM/Qwen3-TTS/issues/14#issuecomment-3789452120"&gt;base models don't support "style-guidance"&lt;/a&gt; - e.g., you can't tell your fine-tuned model to sound very angry - or very sad.&lt;/p&gt;
&lt;p&gt;For some reason, it's not on the Speech Arena &lt;a href="https://artificialanalysis.ai/text-to-speech/leaderboard?open-weights=true"&gt;leaderboard&lt;/a&gt; for Open Weights models. There are &lt;a href="https://artificialanalysis.ai/text-to-speech/model-families/qwen"&gt;two entries&lt;/a&gt; in the ranking - not sure what model they refer to. But from my experience, it's not all about the model. The inference around it and how it "behaves in the wild" is what matters most and Qwen3-TTS delivers.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/104/tts-leaderboard-annotated.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/104/tts-leaderboard-annotated.webp" alt="Annotated open weights TTS leaderboard" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
The open weights TTS leaderboard with a bit of context.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;I spent a morning getting a &lt;a href="https://huggingface.co/buckets/duarteocarmo/voice/tree/samples"&gt;dataset&lt;/a&gt; ready for fine-tuning by reading a couple of articles out loud, and generating training samples with &lt;a href="https://goodsnooze.gumroad.com/l/macwhisper"&gt;MacWhisper&lt;/a&gt; and &lt;a href="https://huggingface.co/nvidia/parakeet-tdt-0.6b-v2"&gt;Parakeet&lt;/a&gt;. I tweaked some of the &lt;a href="https://github.com/duarteocarmo/podcaster/tree/master/finetune_qwen"&gt;fine-tuning code&lt;/a&gt; to avoid the acceleration bug, and trained a 0.6B base model on a &lt;a href="https://www.scaleway.com/en/h100/"&gt;Scaleway H100&lt;/a&gt;. I updated my &lt;a href="https://github.com/duarteocarmo/podcaster"&gt;podcaster&lt;/a&gt; package so that it runs the fine-tuned model on &lt;a href="https://modal.com/"&gt;Modal&lt;/a&gt;. Instead of running on scheduled GitHub actions (we all know how those go) - it now runs fully on my &lt;a href="https://coolify.io/"&gt;Coolify&lt;/a&gt; instance.&lt;/p&gt;
&lt;p&gt;Here's an example article transcription with the old vs. the new model:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chatterbox (original - not the new ones)&lt;/strong&gt;
&lt;audio controls style="width: 75%; display: block; margin-top: 0.5rem" preload="metadata"&gt;&lt;source src="https://duarteocarmo.com/images/104/chatterbox.mp3" type="audio/mpeg"&gt;&lt;/audio&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Qwen3-TTS 0.6B fine-tune - 200 samples, 6 epochs&lt;/strong&gt; - &lt;a href="https://huggingface.co/duarteocarmo/qwen_tts_finetune_0.6B_e10_l1e6"&gt;Hugging Face&lt;/a&gt; &lt;sup id="sf-tts-doesnt-suck-anymore-1-back"&gt;&lt;a href="#sf-tts-doesnt-suck-anymore-1" class="simple-footnote" title=" I trained on only 30 minutes of audio - I should add a lot more in the future to make the audio even better. Might sound a bit less like me - but certainly cleaner to listener's ear imo. I might upgrade to the 1.7B model to make it even better. "&gt;1&lt;/a&gt;&lt;/sup&gt;
&lt;audio controls style="width: 75%; display: block; margin-top: 0.5rem" preload="metadata"&gt;&lt;source src="https://duarteocarmo.com/images/104/qwen3-tts.mp3" type="audio/mpeg"&gt;&lt;/audio&gt;&lt;/p&gt;
&lt;p&gt;TTS is moving fast. There are a lot of things I haven't tested yet. &lt;a href="https://github.com/k2-fsa/OmniVoice"&gt;OmniVoice&lt;/a&gt; is one of them, StepFun's &lt;a href="https://github.com/stepfun-ai/Step-Audio-EditX"&gt;Step-Audio-EditX&lt;/a&gt; is another.&lt;/p&gt;
&lt;p&gt;But the main takeaway is that TTS &lt;em&gt;doesn't&lt;/em&gt; suck anymore. We have options now.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-tts-doesnt-suck-anymore-1"&gt; I trained on only 30 minutes of audio - I should add a lot more in the future to make the audio even better. Might sound a bit &lt;em&gt;less&lt;/em&gt; like me - but certainly cleaner to listener's ear imo. I might upgrade to the 1.7B model to make it even better.  &lt;a href="#sf-tts-doesnt-suck-anymore-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>The largest open pretraining dataset for European Portuguese</title><link href="https://duarteocarmo.com/blog/the-largest-open-pretraining-dataset-for-european-portuguese.html" rel="alternate"/><published>2026-05-12T00:00:00+02:00</published><updated>2026-05-12T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-05-12:/blog/the-largest-open-pretraining-dataset-for-european-portuguese.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/103/semester_educational_score_summary.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/103/semester_educational_score_summary.webp" alt="Semester educational score summary for Bagaço v2" style="max-width:100%;border-radius: 2px;"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Educational score over time vs. document count for Bagaco v2
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;A couple of months ago I released &lt;a href="https://duarteocarmo.com/blog/bagaco-a-pretraining-dataset-for-european-portuguese.html"&gt;Bagaço&lt;/a&gt; - a pretraining dataset for European Portuguese. The idea was simple: take the &lt;a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb-2"&gt;FineWeb 2&lt;/a&gt; dataset, limit it to web pages that look like they came from Portugal, and classify them into categories …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/103/semester_educational_score_summary.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/103/semester_educational_score_summary.webp" alt="Semester educational score summary for Bagaço v2" style="max-width:100%;border-radius: 2px;"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Educational score over time vs. document count for Bagaco v2
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;A couple of months ago I released &lt;a href="https://duarteocarmo.com/blog/bagaco-a-pretraining-dataset-for-european-portuguese.html"&gt;Bagaço&lt;/a&gt; - a pretraining dataset for European Portuguese. The idea was simple: take the &lt;a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb-2"&gt;FineWeb 2&lt;/a&gt; dataset, limit it to web pages that look like they came from Portugal, and classify them into categories (Sports, Culture, etc.) and an educational score.&lt;/p&gt;
&lt;p&gt;Pulling the European Portuguese from the wider corpus was a bit of a frustrating experience. It's a bit like finding a needle in the haystack. I avoided the problem, and just included anything with a &lt;code&gt;.pt&lt;/code&gt; domain in the URL. But that didn't feel like it was enough.&lt;/p&gt;
&lt;p&gt;Which led me to the next phase: &lt;a href="https://duarteocarmo.com/blog/portuguese-variety-identification-the-bitter-lesson.html"&gt;European Portuguese variety identification&lt;/a&gt;. Or – in other words – spotting European Portuguese in the wild. After learning some bitter lessons, I built two FastText-based classifiers that achieved SOTA performance, but with 10x the throughput. So that I could run the classifier at scale.&lt;/p&gt;
&lt;p&gt;With those two pieces in place, maybe you guessed where this was going: &lt;a href="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco2"&gt;Bagaço v2&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;iframe
  src="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco2/embed/viewer/all/train"
  frameborder="0"
  width="100%"
  height="560px"
&gt;&lt;/iframe&gt;
&lt;/p&gt;
&lt;p&gt;Bagaço v2 is – to my knowledge – the largest open-source pretraining dataset for European Portuguese. 33M documents, 37GB of text, approximately &lt;strong&gt;9.3 billion tokens&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It takes the Portuguese split from &lt;a href="https://huggingface.co/datasets/uonlp/CulturaX"&gt;CulturaX&lt;/a&gt;, and uses the classifier I built to only keep the European Portuguese documents (with confidence above 70%) – like before – it also gives each document an educational score and content category.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/103/top_educational_score_domains_table.webp" target="_blank"&gt;
  &lt;img src="https://duarteocarmo.com/images/103/top_educational_score_domains_table.webp" alt="Top educational score domains in Bagaço v2" style="max-width:80%;border-radius: 2px;"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Top domains by educational score in Bagaco v2.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;The dataset is available &lt;a href="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco2"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I recently wrote about &lt;a href="https://duarteocarmo.com/blog/amalia-and-the-future-of-european-portuguese-llms.html"&gt;AMÁLIA and the future of European Portuguese LLMs&lt;/a&gt;. This most recent effort used Arquivo.pt as a data source, and collected 5.8B tokens. Bagaço v2 almost doubles that amount — and is &lt;em&gt;completely&lt;/em&gt; open-source. One of the main conclusions of the article is that to build a strong European Portuguese LLM, we need the right data. &lt;/p&gt;
&lt;p&gt;My hope is that Bagaço v2 takes us one step closer.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Retrospectiva #7</title><link href="https://duarteocarmo.com/blog/retrospectiva-7.html" rel="alternate"/><published>2026-05-04T00:00:00+02:00</published><updated>2026-05-04T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-05-04:/blog/retrospectiva-7.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/102/hermes.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/102/hermes.webp" alt="Hermes Agent in Telegram" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
My Hermes agent – Saramago – giving me coaching feedback and looking for the ideal flights for an upcoming trip.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;Another month bites the dust. We've been back in Denmark for the past couple of weeks. I often joke that these are the only months in the year where Copenhagen is &lt;em&gt;actually …&lt;/em&gt;&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/102/hermes.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/102/hermes.webp" alt="Hermes Agent in Telegram" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
My Hermes agent – Saramago – giving me coaching feedback and looking for the ideal flights for an upcoming trip.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;Another month bites the dust. We've been back in Denmark for the past couple of weeks. I often joke that these are the only months in the year where Copenhagen is &lt;em&gt;actually&lt;/em&gt; worth it. At least weather-wise. &lt;/p&gt;
&lt;p&gt;I've ditched the running leggings, and we're back in shorts. Running has been consistent, although not as much as I'd like. I've got a new virtual coach helping me stay consistent and on top of things. More on that below.&lt;/p&gt;
&lt;p&gt;We're deep in conference season. &lt;a href="https://www.ai.engineer/europe"&gt;AI Engineer Europe&lt;/a&gt; was fun, until the whole family got sick in a small London hotel. This week I'm travelling to &lt;a href="https://www.datamakersfest.com/"&gt;Data Makers Fest&lt;/a&gt; in Porto to give a talk about... I'll publish the slides in &lt;a href="/talks"&gt;/talks&lt;/a&gt; in due time. &lt;/p&gt;
&lt;h2 id="using"&gt;Using&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://hermes-agent.nousresearch.com/docs" target="_blank"&gt;Hermes Agent&lt;/a&gt;&lt;/strong&gt;: You've heard of &lt;a href="https://openclaw.ai/"&gt;OpenClaw&lt;/a&gt;, the famous assistant everyone's talking about. It didn't hit home for me. For the past month though, I've been running the Hermes Agent on a VPS and talking to it via Telegram. I can't believe I'm saying this, but I'm hooked. Comparing flight options, coaching me through my running plans, organizing my calendar, sending emails, tracking calories, you name it. Hermes has handled it. Beware though. None of this works out of the box. I've had to tweak, customize, spend hours making sure it works exactly like I want. It's powered by two open-weight Chinese models: &lt;a href="https://artificialanalysis.ai/models/kimi-k2-6"&gt;Kimi 2.6&lt;/a&gt;, and &lt;a href="https://artificialanalysis.ai/models/deepseek-v4-pro"&gt;DeepSeek V4 Pro&lt;/a&gt;. I'll write more about this soon.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.bevel.health/" target="_blank"&gt;Bevel&lt;/a&gt;&lt;/strong&gt;: I spent two weeks testing Bevel out. Their promise is a Whoop competitor (they are even &lt;a href="https://www.reddit.com/r/bevelhealth/comments/1sfmxkh/whoop_suing_bevel/"&gt;getting sued&lt;/a&gt; by them) - without the steep price tag. You can use your Apple Watch, Oura, or Garmin, and they'll deliver all the health insights you might possibly need. The design of the app is clean and polished, a much better experience than any of the usual players. The AI assistant story is... messy. The intentions are great, but unlike the clean and polished design of the app, the whole assistant thing looks half baked. Missing to reference plans I built. Derailing. I've replaced it with Hermes. Still worth the mention.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.scaleway.com/" target="_blank"&gt;Scaleway&lt;/a&gt;&lt;/strong&gt;: We need more and better European AI. Everything is too US or China focused. For the past 2/3 years, whenever I needed a GPU I went with &lt;a href="https://vast.ai/"&gt;Vast.ai&lt;/a&gt;. Don't get me wrong - it's a great, cheap, and &lt;em&gt;mostly&lt;/em&gt; reliable service. But as I deploy more and more GPU based workloads, I've longed for something EU-based. Scaleway is exactly that. With data centers in France and Poland, the machines have been extremely reliable. It's &lt;a href="https://gpus.io/en/gpus/h100"&gt;not the cheapest out there&lt;/a&gt;, but it's European, and that's a price I'm increasingly willing to pay.&lt;/p&gt;
&lt;h2 id="reading"&gt;Reading&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Note: There's now a &lt;a href="/books"&gt;/books&lt;/a&gt; page in this blog, where you can see most of the books I've read.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.penguin.co.uk/books/450874/anaximander-by-rovelli-carlo/9781802063042" target="_blank"&gt;Anaximander - Carlo Rovelli&lt;/a&gt;&lt;/strong&gt;: I picked up this small book when taking a small break from &lt;a href="https://www.ai.engineer/europe"&gt;AI Engineer Europe&lt;/a&gt; at a &lt;a href="https://maps.app.goo.gl/EZeSZN7XtT5zhQrH8"&gt;Foyles&lt;/a&gt; book store in London without many hopes for it. Carlo Rovelli is an incredibly talented writer and storyteller. I got completely immersed in how the ancient Greeks looked at the world. Is the earth floating? Does the sun go around it? How did written language come about? Why was China behind? Why Greece? How does science progress? These are just some of the questions Carlo dwells on. And he does so &lt;em&gt;beautifully&lt;/em&gt;. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.mondadori.it/libri/lifestyle-medicine-valerio-rosso/" target="_blank"&gt;Lifestyle Medicine - Valerio Rosso&lt;/a&gt;&lt;/strong&gt;: I wanted to read a first book in Italian, by an Italian author. The ideas in it were interesting, but it was a bit repetitive. It argues for the idea of the 7 Pillars for a great life: Letting go of addictions, stress management, physical activity, nutrition, sleep hygiene, nurturing relationships, and optimizing your environment. It was recommended by an Italian blogger I follow. Interesting, but a bit too dense for my taste. &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"And so the tech industry is rushing forward to put AI everywhere at enormous cost — energy, emissions, manufacturing capacity, the ability to buy RAM — and locked into the narrow framework of software brain without realizing they are also asking people to be fundamentally less human. They then sit around wondering why everyone hates them" - Nilay Patel&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.theverge.com/decoder-podcast-with-nilay-patel/657621/the-people-do-not-yearn-for-automation" target="_blank"&gt;THE PEOPLE DO NOT YEARN FOR AUTOMATION - Nilay Patel&lt;/a&gt;&lt;/strong&gt;: I resonate with a lot of this. Everyone is being told they have to use AI or become obsolete. The effect: everyone is starting to &lt;em&gt;hate&lt;/em&gt; AI. The tech industry is forcing an idea onto people, and no-one wants to buy the future they are selling.&lt;/p&gt;
&lt;h2 id="listening"&gt;Listening&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.dwarkesh.com/p/jensen-huang" target="_blank"&gt;Jensen Huang – TPU competition, why we should sell chips to China, &amp;amp; Nvidia’s supply chain moat&lt;/a&gt;&lt;/strong&gt;: Sometimes, Dwarkesh's &lt;a href="https://www.dwarkesh.com/"&gt;podcast&lt;/a&gt; is a bit too AGI pilled for my taste. Other times, it just nails it. This was one of those times. I loved the discussion: particularly the part about China. Arguments can be heated – because people care – but they can still be civilized and interesting. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://newsletter.pragmaticengineer.com/p/building-pi-and-what-makes-self-modifying" target="_blank"&gt;Building Pi, and what makes self-modifying software so fascinating&lt;/a&gt;&lt;/strong&gt;: Two of my favourite people on the pragmatic side of LLMs. Mario and Armin talk about building Pi, and how human judgement still matters in the age of LLMs. Matters a lot, actually.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.youtube.com/watch?v=qOriuK6xDDA" target="_blank"&gt;vinyl house mix from the living room&lt;/a&gt;&lt;/strong&gt;: I saw someone recommend &lt;a href="https://www.brain.fm/"&gt;Brain.fm&lt;/a&gt; the other day on X. Don't listen to generic AI bs. There's so much good music out there. Don't have good taste? Then listen to someone that does. That's the role of a DJ. And Chris Luno is one of my favourites. &lt;/p&gt;
&lt;h2 id="watching"&gt;Watching&lt;/h2&gt;
&lt;p&gt;&lt;br&gt;
&lt;lite-youtube videoid="jP3eesQa3fo"&gt;&lt;/lite-youtube&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.youtube.com/watch?v=jP3eesQa3fo" target="_blank"&gt;I Spent £300 on Chinese Running Shoes, Were They Any Good?&lt;/a&gt;&lt;/strong&gt;: If you run a lot, you probably also buy a lot of running shoes. I just had a brand new pair of New Balance shoes get the sole completely ruined after only two months. It made me think - what else is out there? What if I didn't go mainstream this time? &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.rottentomatoes.com/tv/the_white_lotus/s03" target="_blank"&gt;White Lotus - Season 3&lt;/a&gt;&lt;/strong&gt;: Not as great as the first two seasons, but definitely worth the watch. Had Vitto and me glued to the screen and making strong bets on who we think will die in the end - as normal couples do. She always wins the bet - I always try to cheat my way out. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.rottentomatoes.com/m/the_plastic_detox" target="_blank"&gt;The Plastic Detox&lt;/a&gt;&lt;/strong&gt;: We thought this was going to be another brainless Netflix documentary. The reality was quite different. It's not a super scientific experiment. But you definitely don't look at the world with the same eyes after watching it.&lt;/p&gt;
&lt;p&gt;See you next month!&lt;/p&gt;</content><category term="blog"/></entry><entry><title>AMÁLIA and the future of European Portuguese LLMs</title><link href="https://duarteocarmo.com/blog/amalia-and-the-future-of-european-portuguese-llms.html" rel="alternate"/><published>2026-04-24T00:00:00+02:00</published><updated>2026-04-24T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-04-24:/blog/amalia-and-the-future-of-european-portuguese-llms.html</id><summary type="html">&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Am%C3%A1lia_Rodrigues" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/101/hero.webp" alt="Amália Rodrigues promotional photograph by Augusto Cabrita" style="max-width:100%;border-radius: 2px;"&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Update July 2026:&lt;/strong&gt; The AMÁLIA models have now been released! You can find the weights/datasets on &lt;a href="https://huggingface.co/amalia-llm"&gt;Hugging Face&lt;/a&gt;. Some parts of this blog post might be out of date. So beware!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In December 2024, the Portuguese government &lt;a href="https://portugal.gov.pt/gc24/comunicacao/noticias/modelo-de-linguagem-em-grande-escala-para-a-lingua-portuguesa"&gt;announced&lt;/a&gt; AMÁLIA: a 5.5 Million Euro investment on a large-scale LLM …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Am%C3%A1lia_Rodrigues" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/101/hero.webp" alt="Amália Rodrigues promotional photograph by Augusto Cabrita" style="max-width:100%;border-radius: 2px;"&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Update July 2026:&lt;/strong&gt; The AMÁLIA models have now been released! You can find the weights/datasets on &lt;a href="https://huggingface.co/amalia-llm"&gt;Hugging Face&lt;/a&gt;. Some parts of this blog post might be out of date. So beware!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In December 2024, the Portuguese government &lt;a href="https://portugal.gov.pt/gc24/comunicacao/noticias/modelo-de-linguagem-em-grande-escala-para-a-lingua-portuguesa"&gt;announced&lt;/a&gt; AMÁLIA: a 5.5 Million Euro investment on a large-scale LLM for European Portuguese&lt;sup id="sf-amalia-and-the-future-of-european-portuguese-llms-1-back"&gt;&lt;a href="#sf-amalia-and-the-future-of-european-portuguese-llms-1" class="simple-footnote" title=" When I refer to Portuguese in this post, I mean European Portuguese - it's just shorter - get over it."&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The other day, while building an &lt;a href="https://doc-public-s3.duarteocarmo.com/pt-llm.html"&gt;overview&lt;/a&gt; of the different Portuguese NLP efforts, I stumbled upon the &lt;a href="https://arxiv.org/pdf/2603.26511"&gt;technical report&lt;/a&gt;! I couldn't believe my eyes. Much to talk about! Let's get straight to it!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Actually&lt;/em&gt;, before we do. A quick disclaimer: AMÁLIA is an impressive piece of work. And the researchers should be very proud. But when the investment from the state is this significant, the entire country is the recipient of the work - and so I think it's only fair to ask some hard questions. If you participated on the project and are reading this: Thank you for your work!&lt;/p&gt;
&lt;p&gt;Alright - now let's get to it. &lt;/p&gt;
&lt;h2 id="amalia-in-a-nutshell"&gt;AMÁLIA in a nutshell&lt;/h2&gt;
&lt;p&gt;AMÁLIA is "a fully open source Large Language Model (LLM) for European Portuguese". The goal is simple: to create an LLM that treats European Portuguese as a first-class citizen. Italy, for example - did something similar with &lt;a href="https://aclanthology.org/2024.clicit-1.77.pdf"&gt;Minerva&lt;/a&gt;. AMÁLIA is a result of a collaboration between several top tier Portuguese Universities and Research Labs (NOVA, IST, IT, and FCT). &lt;/p&gt;
&lt;p&gt;Contrary to what I would have expected, AMÁLIA is &lt;em&gt;not&lt;/em&gt; trained from scratch. It's a &lt;em&gt;continuation&lt;/em&gt; of the pre-training phase of &lt;a href="https://eurollm.io/"&gt;EuroLLM&lt;/a&gt;: an earlier effort (with a lot of Portuguese manpower!). To my understanding, the architecture is the &lt;a href="https://arxiv.org/html/2506.04079v1#S2.SS2"&gt;same&lt;/a&gt; as EuroLLM, with some slight modifications to the context length and RoPE scaling. &lt;/p&gt;
&lt;p&gt;Now, how does AMÁLIA focus on Portuguese? One word: &lt;strong&gt;Data&lt;/strong&gt;. Across every different training stage they tried to increase the share of European Portuguese data the model was trained on. During pre-training they used &lt;a href="https://arquivo.pt/"&gt;Arquivo.pt&lt;/a&gt; data, during supervised fine tuning (SFT) they synthetically generated Portuguese data, and during preference training they sub-sampled some of the data from the SFT phase. &lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/101/benchmark-results.webp" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/101/benchmark-results.webp" alt="AMÁLIA benchmark results on PT-PT benchmarks" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Click to open the full resolution version in a new tab. Reconstructed from the AMÁLIA and ALBA papers.
&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;Training is interesting and all, but even more interesting is to measure if what was trained was any good. Which for this particular case, can be &lt;a href="https://duarteocarmo.substack.com/p/a-benchmark-for-language-models-on"&gt;especially challenging.&lt;/a&gt; The team created four new benchmarks specific for European Portuguese. The most prominent one of these is &lt;a href="https://arxiv.org/pdf/2603.26516"&gt;ALBA&lt;/a&gt;. &lt;/p&gt;
&lt;h2 id="how-open-source-really"&gt;How open source, really?&lt;/h2&gt;
&lt;p&gt;The standard for fully open models is &lt;a href="https://allenai.org/olmo"&gt;Olmo&lt;/a&gt;. Olmo doesn't lead benchmarks. That's not the point. The point of Olmo is to be &lt;em&gt;extremely&lt;/em&gt; open. Just browse their &lt;a href="https://arxiv.org/pdf/2512.13961"&gt;technical report&lt;/a&gt;. Weights? Open. Data? Open. Code? Open. Training logs? Open. Everything is publicly accessible. Which makes it an incredible resource.&lt;/p&gt;
&lt;p&gt;At the moment I'm writing this, very little of AMÁLIA is open. I could not find the model weights, data, training logs, or new benchmarks listed anywhere. The Arquivo.pt processing &lt;a href="https://github.com/AMÁLIA-LLM/arquivo_processing"&gt;scripts&lt;/a&gt; are open, but the resulting dataset itself is nowhere to be found. For now we have some &lt;a href="https://github.com/AMÁLIA-LLM/"&gt;GitHub repos&lt;/a&gt;. &lt;a href="https://opensource.org/ai/open-weights"&gt;In the era of many "open weights" but few "open source" LLMs&lt;/a&gt;, it has &lt;strong&gt;never&lt;/strong&gt; been more important to put these things out there, and put them out there fast. &lt;/p&gt;
&lt;p&gt;Maybe it's a matter of time. Maybe there's something beyond my understanding as to why we still have no model weights. Maybe it's a research-in-progress. &lt;/p&gt;
&lt;p&gt;But even if they released weights tomorrow, I'm not sure I'm completely sold on the approach. &lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/101/pretraining-mix.webp" alt="AMÁLIA continued pretraining data distribution" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;h2 id="how-much-portuguese-data-for-a-portuguese-model"&gt;How much Portuguese data for a Portuguese model?&lt;/h2&gt;
&lt;p&gt;So how much actual Portuguese data was used in training this model?&lt;/p&gt;
&lt;p&gt;According to the report the extended pre-training was a total of 107B tokens. Of those, the only clearly European Portuguese component is the 5.8B tokens from Arquivo.pt. That's &lt;strong&gt;around 5.5%&lt;/strong&gt;, which is not a lot.&lt;/p&gt;
&lt;p&gt;To be fair - there surely is some Portuguese data in the &lt;a href="https://arxiv.org/pdf/2506.04079"&gt;EuroLLM&lt;/a&gt; mixture already. But we don't know (1) how much, (2) and if it's actually European Portuguese or something else.&lt;/p&gt;
&lt;p&gt;On the SFT side, the percentage is higher - more like 17-18%. But is that enough? To be transparent, I don't have a completely clear picture of how much European Portuguese is in total in this model. And I would like to. &lt;/p&gt;
&lt;p&gt;Impressively, AMÁLIA beats SOTA models like Qwen 3-8B on most Portuguese benchmarks (big win!)&lt;sup id="sf-amalia-and-the-future-of-european-portuguese-llms-2-back"&gt;&lt;a href="#sf-amalia-and-the-future-of-european-portuguese-llms-2" class="simple-footnote" title="On another note - not sure why we included Olmo 2-7B and not Olmo 3-7B in the comparison?"&gt;2&lt;/a&gt;&lt;/sup&gt;. But Qwen 3-8B still beats us on ALBA for example, why? Is it because they did some Portuguese specific training? Unlikely. Which makes me wonder: How much more could we benefit from additional pre-training data in Portuguese? &lt;/p&gt;
&lt;p&gt;I can only speculate. Are we even optimizing for the right thing? &lt;/p&gt;
&lt;h2 id="what-should-we-be-optimizing-for"&gt;What should we be optimizing for?&lt;/h2&gt;
&lt;p&gt;The AMÁLIA team created four new benchmarks for European Portuguese. They cover a lot of ground. They focus on grammar, syntax, general knowledge, and (important!) whether it has a significant bias towards Brazilian Portuguese. &lt;/p&gt;
&lt;p&gt;But I think there's a dimension the team missed: Are we actually measuring &lt;em&gt;how much the model knows about Portugal?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It's a great opportunity to show that a model that is smaller, but has much more &lt;em&gt;intrinsic&lt;/em&gt; knowledge about Portugal. Even when comparing with similar (or even larger) models. I don't think any of the benchmarks captures this dimension. Portuguese exams help - but don't fully solve the problem. I'm thinking: "What's the most famous dessert served in Aveiro?", "Who was the president of Portugal between 1978 and 1985?". &lt;/p&gt;
&lt;p&gt;But I also think the best place to tackle this is the pre-training stage. This would require much &lt;em&gt;more&lt;/em&gt; Portuguese data. And the team acknowledges that. &lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;First of all - I hesitated to write this one. I don't like to criticize anyone's work, especially on the internet. I'm happy Portugal invested in this. We have an incredibly talented team, and they deserve credit. &lt;/p&gt;
&lt;p&gt;Second - it's very challenging to make a LARGE language model for such a TINY country and "language". The data is limited, but it's &lt;a href="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco"&gt;out there&lt;/a&gt;, we just need to get &lt;a href="https://duarteocarmo.com/blog/portuguese-variety-identification-the-bitter-lesson"&gt;creative&lt;/a&gt; on how we find it. &lt;/p&gt;
&lt;p&gt;Third - this is a good first step towards an exciting direction. The future is bright for European Portuguese LLMs! We just need to keep our minds, weights, data, and evals - &lt;strong&gt;open&lt;/strong&gt;. &lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-amalia-and-the-future-of-european-portuguese-llms-1"&gt; When I refer to Portuguese in this post, I mean European Portuguese - it's just shorter - get over it. &lt;a href="#sf-amalia-and-the-future-of-european-portuguese-llms-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-amalia-and-the-future-of-european-portuguese-llms-2"&gt;On another note - not sure why we included Olmo 2-7B and not Olmo 3-7B in the comparison? &lt;a href="#sf-amalia-and-the-future-of-european-portuguese-llms-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>Retrospectiva #6</title><link href="https://duarteocarmo.com/blog/retrospectiva-6.html" rel="alternate"/><published>2026-04-03T00:00:00+02:00</published><updated>2026-04-03T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-04-03:/blog/retrospectiva-6.html</id><summary type="html">&lt;p&gt;&lt;img alt="Ponte 25 de Abril over the Tagus river" src="https://duarteocarmo.com/images/100/ponte-25-de-abril.webp" /&gt;&lt;/p&gt;
&lt;p&gt;Well, now would you look at that. Article 100 on this website. Ten years of writing on this small corner of the web. I'm writing this month's newsletter from my favourite place: the airplane.&lt;/p&gt;
&lt;p&gt;Happy Easter if you celebrate. With clients spread across time zones, I didn't really get the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="Ponte 25 de Abril over the Tagus river" src="https://duarteocarmo.com/images/100/ponte-25-de-abril.webp" /&gt;&lt;/p&gt;
&lt;p&gt;Well, now would you look at that. Article 100 on this website. Ten years of writing on this small corner of the web. I'm writing this month's newsletter from my favourite place: the airplane.&lt;/p&gt;
&lt;p&gt;Happy Easter if you celebrate. With clients spread across time zones, I didn't really get the time. Next week I'll be at &lt;a href="https://www.ai.engineer/london" target="_blank"&gt;AI Engineer London&lt;/a&gt;. Email me if you're coming too!&lt;/p&gt;
&lt;p&gt;I'm experimenting with a &lt;em&gt;slightly&lt;/em&gt; different format this week.&lt;/p&gt;
&lt;h2 id="using"&gt;Using&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://shittycodingagent.ai/" target="_blank"&gt;Pi.dev&lt;/a&gt;&lt;/strong&gt;: After watching &lt;a href="https://www.youtube.com/watch?v=0RLIlNWv1xo"&gt;this talk&lt;/a&gt; on how some people use Pi, I decided to (re-)double down. It's become my main driver. I've created a nice collection of &lt;a href="https://github.com/duarteocarmo/dotfiles/tree/master/.agents/skills"&gt;skills&lt;/a&gt; (some private), &lt;a href="https://github.com/duarteocarmo/dotfiles/tree/master/.pi/agent/extensions"&gt;extensions&lt;/a&gt;, and even ported my favorite &lt;a href="https://github.com/duarteocarmo/dotfiles/tree/master/.pi/agent/themes"&gt;theme&lt;/a&gt; to it. An &lt;a href="/blog/how-to-police-your-agents.html"&gt;agent harness&lt;/a&gt; is a bit like an editor: it needs to be &lt;em&gt;yours&lt;/em&gt;. I want to &lt;code&gt;/preview&lt;/code&gt; a longer agent plan in a nice web page. I want to answer questions in a nice interface with &lt;code&gt;/answer&lt;/code&gt;, I want the theme to adapt to my macOS system theme. And, most of all, I want to be able to use it with ANY model. Even &lt;a href="https://github.com/duarteocarmo/dotfiles/blob/master/.pi/agent/models.json#L9"&gt;local ones&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://developers.openai.com/codex/app/" target="_blank"&gt;Codex&lt;/a&gt;&lt;/strong&gt;: Some things need my full attention. Others don't. When it's a quick fix, a breaking test, a simpler analysis, or something I can run in parallel, Codex has been my go-to. You can preview diffs, open in a terminal, create PRs, all from the same place. I tend to throw at it the smaller parallel things that are not "high risk".&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.hevyapp.com/" target="_blank"&gt;Hevy&lt;/a&gt;&lt;/strong&gt;: I &lt;a href="/blog/an-opinionated-running-dashboard.html"&gt;continue to run&lt;/a&gt;. Not as much as I'd like. As you get older, weight lifting and strength training become increasingly important. After freestyling my gym routine for the past years, I wanted something a bit more sophisticated. After testing 3-4 apps, I landed on Hevy. It's paid but not expensive. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://apps.apple.com/us/app/snapseed/id439438619" target="_blank"&gt;Snapseed&lt;/a&gt;&lt;/strong&gt;: My inspiration to take &lt;a href="/photos"&gt;photos&lt;/a&gt; comes and goes. But I need an editing app that makes the ugly Danish sky just a tad prettier. The previous version of Snapseed looked largely abandoned so I resorted to &lt;a href="https://darkroom.co/"&gt;Darkroom&lt;/a&gt;. Now that everything is a subscription everywhere I started looking again. The new Snapseed app is excellent. &lt;/p&gt;
&lt;h2 id="reading"&gt;Reading&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.goodreads.com/book/show/222725518-empire-of-ai" target="_blank"&gt;Empire of AI - Karen Hao&lt;/a&gt;&lt;/strong&gt;: I tested a new reading routine this month: One chapter a day. Ended up finishing this one. The book has some interesting tidbits about the history of OpenAI and how it all came to be. It also has some slightly deeper commentary on the ethics of AI models and infrastructure build outs. Unfortunately, it had a bit too much &lt;em&gt;TMZ-style&lt;/em&gt; tech drama I don't really find interesting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.baseten.co/inference-engineering/" target="_blank"&gt;Inference Engineering - Philip Kiely&lt;/a&gt;&lt;/strong&gt;: I picked this one up on my &lt;a href="/blog/goodbye-kindle-i-dont-think-ill-miss-you.html"&gt;Palma&lt;/a&gt;. Even though I just started, I can already recommend it. As I work with larger models, datasets, and infrastructure - it's a great field guide.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"If the cluster is not on fire, you are not using it enough" &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://emerge-lab.github.io/papers/an-unsolicited-guide-to-good-research.pdf" target="_blank"&gt;An unsolicited guide to good research - Eugene Vinitsky&lt;/a&gt;&lt;/strong&gt;: A great deck on how to do great ML research, and how to find interesting problems. I liked the framing: pragmatic and to the point. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.philschmid.de/kimi-composer-context" target="_blank"&gt;How Kimi, Cursor, and Chrome Train Agentic Models with RL - Phil Schmid&lt;/a&gt;&lt;/strong&gt;: An incredible roundup of the reinforcement learning techniques some of the larger players are using to train models.&lt;/p&gt;
&lt;h2 id="listening"&gt;Listening&lt;/h2&gt;
&lt;p&gt;&lt;br&gt;
&lt;lite-youtube videoid="1bsBBU_ETy8"&gt;&lt;/lite-youtube&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://bit.donado.co/1xAhBA" target="_blank"&gt;The Roving - Bonny Light Horseman&lt;/a&gt;&lt;/strong&gt;: What can I say? After clicking "don't recommend" on those Ruff songs on my Discover Weekly, sometimes Spotify eventually makes you discover something new and interesting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://soundcloud.com/intuition_radio/reverie" target="_blank"&gt;Reverie - Jule&lt;/a&gt;&lt;/strong&gt;: I managed to tone down meetings and put some nice focused stretches of work this month. And a good SoundCloud mix is essential to keep me company.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://podcasts.apple.com/us/podcast/confronting-the-ceo-of-the-ai-company-that-impersonated-me/id1011668648?i=1000756732024" target="_blank"&gt;Confronting the CEO of the AI Company that impersonated me - Decoder&lt;/a&gt;&lt;/strong&gt;: Long-term fan of the Vergecast. I love Nilay's interview style. I do think Shishir held his ground pretty well. An interesting discussion, if we set aside all the drama.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://podcasts.apple.com/us/podcast/the-handyman-of-high-art-tom-sachs-on-why-creativity/id582272991?i=1000752476132" target="_blank"&gt;The Handyman of High Art - Rich Roll&lt;/a&gt;&lt;/strong&gt;: I'm a big fan of Tom Sachs. The NY-based artist that inspired much of Casey Neistat's brutalist aesthetic. And I'm a sucker for brutalism. An interesting look at his life and work.&lt;/p&gt;
&lt;h2 id="watching"&gt;Watching&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.youtube.com/watch?v=YwZR6tc7qYg" target="_blank"&gt;Everything We Got Wrong About Research-Plan-Implement&lt;/a&gt;&lt;/strong&gt;: Loved this one. On how to tackle codebases with agents. Some interesting mentions of: &lt;em&gt;"Hey - we let the agent rip, and actually regretted it. Then we went back, and thought about how to do it properly"&lt;/em&gt;. Agents are likely to eat software, but we are still learning how to deal with it.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;
&lt;lite-youtube videoid="Pdp3p23P-TI"&gt;&lt;/lite-youtube&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.youtube.com/watch?v=Pdp3p23P-TI" target="_blank"&gt;Living in the Matrix - Casey Neistat&lt;/a&gt;&lt;/strong&gt;: From the master, extremely relatable. I also feel like chasing the green quadrant every time. But maybe the other 3 are a necessary evil.&lt;/p&gt;
&lt;p&gt;The flight attendant is about to come by and scream at me for not closing my laptop. See you next month!&lt;/p&gt;</content><category term="blog"/></entry><entry><title>An opinionated running dashboard</title><link href="https://duarteocarmo.com/blog/an-opinionated-running-dashboard.html" rel="alternate"/><published>2026-03-25T00:00:00+01:00</published><updated>2026-03-25T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-03-25:/blog/an-opinionated-running-dashboard.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://tandarunner.duarteocarmo.com/"&gt;
&lt;img src="https://duarteocarmo.com/images/99/calendar.webp" alt="Tanda Runner calendar view" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;As you get older, life becomes complicated. Not in a bad way. There’s just more going on. We don’t all want to run marathons. Maybe you want to run a &lt;a href="https://www.parkrun.com/"&gt;parkrun&lt;/a&gt;. Maybe you want to gradually increase your volume. Maybe you don't want to run at all. Whatever …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://tandarunner.duarteocarmo.com/"&gt;
&lt;img src="https://duarteocarmo.com/images/99/calendar.webp" alt="Tanda Runner calendar view" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;As you get older, life becomes complicated. Not in a bad way. There’s just more going on. We don’t all want to run marathons. Maybe you want to run a &lt;a href="https://www.parkrun.com/"&gt;parkrun&lt;/a&gt;. Maybe you want to gradually increase your volume. Maybe you don't want to run at all. Whatever your running goal is, you &lt;em&gt;should&lt;/em&gt; be able to plan for it.&lt;/p&gt;
&lt;p&gt;In the new version of &lt;a href="https://tandarunner.duarteocarmo.com/"&gt;Tanda Runner&lt;/a&gt; you can plan for &lt;em&gt;whatever&lt;/em&gt; your running goal is. I redesigned all the parts I wasn't a fan of. The result is a redesigned chat interface with an agent that knows about all your runs, and, my favourite: a fully automated - but personalized - running plan generator.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://tandarunner.duarteocarmo.com/"&gt;
&lt;img src="https://duarteocarmo.com/images/99/chat_interface.webp" alt="Tanda Runner chat interface" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;The plan generator analyzes your past activity, understands your running habits, and builds a completely customized running plan just for you. You can take this and export it to your calendar app of choice.&lt;/p&gt;
&lt;p&gt;The technology choices evolved a bit, but not too much. I’m still on my beloved &lt;a href="https://www.djangoproject.com/"&gt;Django&lt;/a&gt; + &lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt; stack. The LLM models are served through &lt;a href="https://openrouter.ai/"&gt;OpenRouter&lt;/a&gt;, and tracing is sent automatically to &lt;a href="https://openrouter.ai/docs/guides/features/broadcast/weave"&gt;W&amp;amp;B Weave&lt;/a&gt;. The agent itself is built with &lt;a href="https://ai.pydantic.dev/"&gt;PydanticAI&lt;/a&gt;, a framework I’ve enjoyed using more and more. &lt;/p&gt;
&lt;p&gt;There aren’t enough niche running tools out there. This one is mine.&lt;/p&gt;
&lt;p&gt;See you out there, runner.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Portuguese variety identification: The bitter lesson</title><link href="https://duarteocarmo.com/blog/portuguese-variety-identification-the-bitter-lesson.html" rel="alternate"/><published>2026-03-09T00:00:00+01:00</published><updated>2026-03-09T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-03-09:/blog/portuguese-variety-identification-the-bitter-lesson.html</id><summary type="html">&lt;div class="iframe-container"&gt;
  &lt;iframe scrolling="no" id="results-frame" src="https://duarteocarmo.com/html/98/euptvid-results.html" title="FastText vs BERT results" loading="lazy"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;Given some text in Portuguese, how easy is it to determine if it's from Brazil or Portugal&lt;sup id="sf-portuguese-variety-identification-the-bitter-lesson-1-back"&gt;&lt;a href="#sf-portuguese-variety-identification-the-bitter-lesson-1" class="simple-footnote" title="Or somewhere else!"&gt;1&lt;/a&gt;&lt;/sup&gt;? For native speakers, this is pretty easy – it's almost a feeling. But for machines: not so much. &lt;/p&gt;
&lt;p&gt;This might seem like a useless problem at first. But in the age of language …&lt;/p&gt;</summary><content type="html">&lt;div class="iframe-container"&gt;
  &lt;iframe scrolling="no" id="results-frame" src="https://duarteocarmo.com/html/98/euptvid-results.html" title="FastText vs BERT results" loading="lazy"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;Given some text in Portuguese, how easy is it to determine if it's from Brazil or Portugal&lt;sup id="sf-portuguese-variety-identification-the-bitter-lesson-1-back"&gt;&lt;a href="#sf-portuguese-variety-identification-the-bitter-lesson-1" class="simple-footnote" title="Or somewhere else!"&gt;1&lt;/a&gt;&lt;/sup&gt;? For native speakers, this is pretty easy – it's almost a feeling. But for machines: not so much. &lt;/p&gt;
&lt;p&gt;This might seem like a useless problem at first. But in the age of language models and some of my research, I've seen this problem more than once.&lt;/p&gt;
&lt;p&gt;When I &lt;a href="/blog/a-benchmark-for-language-models-on-european-portuguese.html"&gt;contributed Portuguese datasets to EuroEval&lt;/a&gt;, I had a hard time finding exclusively European Portuguese data. Most of the Portuguese on the Internet is &lt;em&gt;obviously&lt;/em&gt; of the Brazilian variant. When I created &lt;a href="/blog/bagaco-a-pretraining-dataset-for-european-portuguese.html"&gt;Bagaço&lt;/a&gt;, a pretraining dataset for European Portuguese, the most obvious solution was to filter data for &lt;code&gt;.pt&lt;/code&gt; domains. This works as a proxy, but is far from ideal.&lt;/p&gt;
&lt;p&gt;It stuck in my mind: If we want to build good European Portuguese models we need robust ways of detecting that language variant in the wild.&lt;/p&gt;
&lt;p&gt;In &lt;em&gt;&lt;a href="https://arxiv.org/abs/2502.14394"&gt;Enhancing Portuguese Variety Identification with Cross-Domain Approaches&lt;/a&gt;&lt;/em&gt; the authors take a stab at the problem. They fine-tune &lt;a href="https://huggingface.co/neuralmind/bert-large-portuguese-cased"&gt;BERTimbau&lt;/a&gt; to classify text into PT-PT or PT-BR. The key idea: by hiding specific types of words (places, names, etc.) the model is forced to learn the structural/grammatical differences between the languages. It works well, but there's a problem: It doesn't scale!&lt;/p&gt;
&lt;p&gt;Running a BERT based model to filter pre-training data (at internet scale) could take &lt;em&gt;days&lt;/em&gt; even with dedicated hardware. I needed something as good as their model but that I could run fast for hundreds of Gigabytes of text. Most pre-training datasets use something like &lt;a href="https://fasttext.cc/"&gt;FastText&lt;/a&gt; or &lt;a href="https://github.com/cisnlp/GlotLID"&gt;variants&lt;/a&gt; to filter data by language. It scales really well&lt;sup id="sf-portuguese-variety-identification-the-bitter-lesson-2-back"&gt;&lt;a href="#sf-portuguese-variety-identification-the-bitter-lesson-2" class="simple-footnote" title="So well that forced Claude into building a Rust implementation for it."&gt;2&lt;/a&gt;&lt;/sup&gt; and doesn't need any type of dedicated hardware.&lt;/p&gt;
&lt;p&gt;After some &lt;a href="https://github.com/LIAAD/portuguese_vid/issues/3"&gt;hiccups&lt;/a&gt; (and a lot of tokens) I finally managed to reproduce the results from the paper. Once that was done, I started training a series of FastText models on &lt;a href="https://huggingface.co/datasets/liaad/PtBrVId"&gt;their same dataset&lt;/a&gt;. The results were &lt;em&gt;mediocre&lt;/em&gt; at best. Different configurations of the data, some oversampling, parameter tuning, nothing got close to the baseline performance (theirs).&lt;/p&gt;
&lt;p&gt;Then I thought: what if I just need more data? &lt;/p&gt;
&lt;p&gt;After some digging, I found &lt;a href="https://huggingface.co/datasets/bastao/VeraCruz_PT-BR"&gt;this dataset&lt;/a&gt; from &lt;a href="https://www.linkedin.com/in/fab-bastos/"&gt;Fabio Bastos&lt;/a&gt; - he basically did the exact domain level filtering as &lt;a href="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco"&gt;Bagaço&lt;/a&gt;, but for a much larger dataset: &lt;a href="https://huggingface.co/datasets/uonlp/CulturaX"&gt;CulturaX&lt;/a&gt;. Separating it into Portuguese and Brazilian sources: exactly what I need to train a model. I started training classifiers, and the more data I trained on, the better performance I got. Once I trained on ~6M rows lo and behold: similar performance but &lt;strong&gt;10x&lt;/strong&gt; faster. &lt;/p&gt;
&lt;p&gt;Actually, the quantized version of the model I trained packs a whole lot of punch but in only 70MB! The quantized model is so small it runs in your browser:&lt;/p&gt;
&lt;div class="iframe-container"&gt;
  &lt;iframe scrolling="no" id="demo-frame" src="https://duarteocarmo.com/html/98/demo.html" title="FastText Portuguese variety classifier demo" loading="lazy"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;As a sanity check, I ran the model on a subset of &lt;a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb-2"&gt;FineWeb2&lt;/a&gt; that had been filtered for Portuguese. Here's a snippet of the output:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Loading model...
Downloading parquet...Loaded 33,846 rows
Classifying with threshold=0.7...
Classified 33,846 rows in 30.1s (1,125 rows/sec)
Score distribution:
mean=0.320  median=0.176
Total: 33,846 → PT-PT: 6,102 (18.0% kept)
Random PT-PT URLs:
- https://blogtailors.com/4997524.html
- https://www.bodyboardcenter.com/pt/apparel/734-sen-no-sen-not-diet-atoll-polo
- https://abapinho.com/2017/09/plantuml-finalmente-o-uml-da-para-usar/
- https://www.sabado.pt/portugal/detalhe/como-jose-veiga-denunciou-rui-rangel
- http://www.poadvogados.pt/Areas/InsolvRecoverE/?MOBILE=1
- https://www.viralagenda.com/pt/events/926945/palestra-intervir-com-criancas-e-jovens-em-risco-como-actuar
- http://confessionsfashiongirl.blogspot.com/2009/11/coisinhas-sem-as-quais-nao-podia-viver_08.html
- https://www.powrenism.com/forum/welcome-to-the-forum/exercice-dorsaux-sans-materiel-myogen-dianabol
- https://topbinamvf.web.app/berezny38567def/forex-seminbrio-manchester-1780.html
- https://foreveryoung.sapo.pt/rustic-chicken-ja-conhece-a-nova-proposta-da-mcdonalds/
- https://aralumiar.wordpress.com/2007/01/
- http://iclub.pt/video-do-4o-iclub-dinner/
- http://www.hotfrog.pt/empresa/lisboa/odivelas/avarias-ao-domicilio
- https://www.rowenta.pt/eficiencia-energetica-by-rowenta
- https://codimagem.com/2023/05/12/giros-gr%C3%A1tis-eletr%C3%B4nicos/
- http://apracas.pt/article/list/1/noticias/
- https://www.noticiasaominuto.com/politica/864278/cgtp-diz-que-greve-mostrou-que-autoeuropa-nao-vive-acima-da-lei
- http://nave-azul.blogspot.com/2010/11/portal-101110.html
- http://gbtrabalhoshubj.ischadia.info/mensagem-de-fernando-pessoa-terceira-parte-o-encoberto.html
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As you can see, lots of these would never pop up if we just filtered for &lt;code&gt;.pt&lt;/code&gt; domains. Also, the percentage kept aligns well at ~18% of text in the &lt;code&gt;pt&lt;/code&gt; split being European Portuguese.&lt;/p&gt;
&lt;p&gt;There's a parallel to LLMs in this post. Unlike the original paper, we didn't do any fancy delexicalization, we didn't even curate a pristine mix of data from different sources. We threw 6M rows of web data at the problem, and the problem got fixed. This is a known lesson in the world of computing: &lt;a href="https://www.cs.utexas.edu/~eunsol/courses/data/bitter_lesson.pdf"&gt;it's called the Bitter Lesson&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;The model and code are available on &lt;a href="https://github.com/duarteocarmo/eupt_vid"&gt;GitHub&lt;/a&gt; and &lt;a href="https://huggingface.co/duarteocarmo/fasttext-euptvid"&gt;Hugging Face&lt;/a&gt;.&lt;/p&gt;
&lt;style&gt;
  .iframe-container {
    width: 100%;
    max-width: 800px;
    margin: 2rem auto;
    text-align: center;
  }

  #results-frame {
    width: 100%;
    height: 440px;
    border: none;
    display: block;
    box-sizing: border-box;
  }

  #demo-frame {
    width: 100%;
    height: 420px;
    border: none;
    display: block;
    box-sizing: border-box;
  }

  @media (max-width: 480px) {
    #results-frame { height: 300px; }
    #demo-frame { height: 380px; }
  }
&lt;/style&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-portuguese-variety-identification-the-bitter-lesson-1"&gt;Or &lt;a href="https://en.wikipedia.org/wiki/Portuguese-speaking_world"&gt;somewhere else&lt;/a&gt;! &lt;a href="#sf-portuguese-variety-identification-the-bitter-lesson-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-portuguese-variety-identification-the-bitter-lesson-2"&gt;So well that forced Claude into building a &lt;a href="https://github.com/duarteocarmo/fasttext.rs"&gt;Rust implementation&lt;/a&gt; for it. &lt;a href="#sf-portuguese-variety-identification-the-bitter-lesson-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>Retrospectiva #5</title><link href="https://duarteocarmo.com/blog/retrospectiva-5.html" rel="alternate"/><published>2026-03-01T00:00:00+01:00</published><updated>2026-03-01T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-03-01:/blog/retrospectiva-5.html</id><summary type="html">&lt;p&gt;Happy February! Just like that, we are close to wrapping up the first quarter of 2026. After quite some time roaming around, we finally flew back home to Copenhagen. It's cold, windy, and grey, but it's also calm, organized, and cozy. Most of all: it's &lt;em&gt;home&lt;/em&gt;. &lt;/p&gt;
&lt;p&gt;Another good thing about …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Happy February! Just like that, we are close to wrapping up the first quarter of 2026. After quite some time roaming around, we finally flew back home to Copenhagen. It's cold, windy, and grey, but it's also calm, organized, and cozy. Most of all: it's &lt;em&gt;home&lt;/em&gt;. &lt;/p&gt;
&lt;p&gt;Another good thing about Denmark is that it's one of the &lt;a href="https://www.instagram.com/p/C6oRHlVs4lK/"&gt;flattest&lt;/a&gt; countries in Europe. Perfect for increasing weekly running volume. It's cold, but it's almost time to ditch the running tights! Just a couple months left.&lt;/p&gt;
&lt;h2 id="using"&gt;Using&lt;/h2&gt;
&lt;p&gt;We've been going with Allegra pretty much everywhere. From small walks, to airplanes, to high speed trains. We've been lucky - she's really chill, and sleeps most of the time. But one of our best purchases was the &lt;a href="https://ergobaby.com/baby-carrier/omni/the-omni-360-baby-carrier#color-Midnight%20Blue%20"&gt;Ergo Baby Omni 360&lt;/a&gt;. Vitto managed to find it second-hand at a nice price. But it's been absolutely great to take her everywhere. And it should last us many more months. A strong recommendation for parents out there. &lt;/p&gt;
&lt;div style="display: flex; justify-content: center;"&gt;
&lt;blockquote class="twitter-tweet" data-height="50"&gt;&lt;p lang="en" dir="ltr"&gt;Today is the day I canceled copilot.&lt;a href="https://t.co/OPUIEX5eIe"&gt;https://t.co/OPUIEX5eIe&lt;/a&gt;&lt;/p&gt;&amp;mdash; Duarte (@duarteocarmo) &lt;a href="https://twitter.com/duarteocarmo/status/2027424182846492927?ref_src=twsrc%5Etfw"&gt;February 27, 2026&lt;/a&gt;&lt;/blockquote&gt; &lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;
&lt;/div&gt;

&lt;p&gt;As for tech, two interesting things have been changing in my workflow (which is still &lt;em&gt;unstable&lt;/em&gt;). The first is my increasing usage of coding agents for everyday things. For things that I find myself repeating more and more I've been using &lt;a href="https://agentskills.io/home"&gt;agent skills&lt;/a&gt;. Still bounce around different agent CLIs quite a bit, but I can largely reuse them across tools. For the past month, I've been using a mix of the &lt;a href="https://developers.openai.com/codex/app/"&gt;Codex app&lt;/a&gt;, and the &lt;em&gt;shitty-but-not-so-shitty&lt;/em&gt; coding agent &lt;a href="https://shittycodingagent.ai/"&gt;Pi&lt;/a&gt;. The second interesting thing is the increasing capability of local LLMs. For example, I completely replaced my &lt;a href="https://github.com/features/copilot"&gt;GitHub Copilot&lt;/a&gt; subscription as well, with my own fork of &lt;a href="https://github.com/ggml-org/llama.vim"&gt;llama.vim&lt;/a&gt;, called &lt;a href="https://github.com/duarteocarmo/llama.lua"&gt;llama.lua&lt;/a&gt;. &lt;/p&gt;
&lt;h2 id="reading"&gt;Reading&lt;/h2&gt;
&lt;p&gt;Two long read recommendations for this month. The first is &lt;em&gt;&lt;a href="https://elanapearl.github.io/blog/2024/the-illustrated-alphafold/"&gt;The Illustrated AlphaFold&lt;/a&gt;&lt;/em&gt; by Elana P. Simon. She does a great illustrated rundown of the key inner workings of the famous algorithm from Google. Another recommendation is the latest article from Raschka: &lt;em&gt;&lt;a href="https://sebastianraschka.com/blog/2026/a-dream-of-spring-for-open-weight.html"&gt;A Dream of Spring for Open-Weight LLMs: 10 Architectures from Jan-Feb 2026&lt;/a&gt;&lt;/em&gt;. It's funny how LLM architectures have changed - but really, not all that much. &lt;/p&gt;
&lt;p&gt;&lt;img alt="Alphafold" src="https://duarteocarmo.com/images/97/full_arch_for_labeling.png" /&gt;&lt;/p&gt;
&lt;p&gt;I also recommend the latest &lt;a href="https://www.anthropic.com/news/detecting-and-preventing-distillation-attacks"&gt;bomb from Anthropic&lt;/a&gt; accusing almost every Chinese lab of calling their API to distil their own models. Isn't it funny how large labs train their models on the entire Internet, but are then very much protective when someone starts doing the same with their API? In any case - didn't Anthropic do the &lt;a href="https://www.nytimes.com/2025/09/05/technology/anthropic-settlement-copyright-ai.html"&gt;exact same thing, but with book authors&lt;/a&gt;? &lt;/p&gt;
&lt;p&gt;&lt;img alt="Cartoon" src="https://duarteocarmo.com/images/97/cartoon.jpeg" /&gt;&lt;/p&gt;
&lt;p&gt;Pretty much finished with &lt;em&gt;&lt;a href="https://www.amazon.com/LLMOps-Managing-Language-Models-Production/dp/1098154207"&gt;LLMOps&lt;/a&gt;&lt;/em&gt; from Abi Aryan. Interesting read to consolidate some of the ideas from everything I've been seeing in the field. This month I've picked up &lt;em&gt;&lt;a href="https://gregmckeown.com/books/essentialism/"&gt;Essentialism&lt;/a&gt;&lt;/em&gt; from Greg McKeown, and &lt;em&gt;&lt;a href="https://www.amazon.com/Art-Doing-Science-Engineering-Learning/dp/1732265178"&gt;The Art of Doing Science and Engineering&lt;/a&gt;&lt;/em&gt; from Richard Hamming. Not sure which one will stick yet. &lt;/p&gt;
&lt;h2 id="listening"&gt;Listening&lt;/h2&gt;
&lt;p&gt;I have three podcast recommendations for this month. Unsurprisingly - they are all very much about LLMs, AI, and the future of technology. But I'm sure you were already expecting that. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://overcast.fm/+AAyIRRePJOw"&gt;The Evolution of Reasoning in Small Language Models with Yejin Choi&lt;/a&gt; (TWIML AI Podcast): I'm a big believer in the increasing power of small language models.  &lt;/li&gt;
&lt;li&gt;&lt;a href="https://overcast.fm/+ABKyPKQ2rGU"&gt;The third golden age of software engineering&lt;/a&gt; (The Pragmatic Engineer): How the world of Software is changing, and what to focus on. &lt;/li&gt;
&lt;li&gt;&lt;a href="https://overcast.fm/+AA5AWNElGto"&gt;Jared Sleeper on Which Software Companies Will Survive the “SaaSpocalypse”&lt;/a&gt; (Odd Lots): Software stocks have been dipping, why? &lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="watching"&gt;Watching&lt;/h2&gt;
&lt;p&gt;Every year, I try to completely avoid any sort of news related to Formula 1. I don't watch races during the year, I block out everything I possibly can about it. Why? Because I'm always waiting for the next season of Drive to Survive. You should too. Even if you're not a fan of cars (I've never been). But I'm a fan of competitive sports, and this show captures that essence flawlessly. &lt;/p&gt;
&lt;iframe width="100%" height="315" src="https://www.youtube.com/embed/T3nrpj38zSk?si=VEvyQCd3KFbZBqhV" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;

&lt;p&gt;That's it for the February edition of Retrospectiva, see you next month!&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Bagaço: A pretraining dataset for European Portuguese</title><link href="https://duarteocarmo.com/blog/bagaco-a-pretraining-dataset-for-european-portuguese.html" rel="alternate"/><published>2026-02-23T00:00:00+01:00</published><updated>2026-02-23T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-02-23:/blog/bagaco-a-pretraining-dataset-for-european-portuguese.html</id><summary type="html">&lt;p&gt;Let's say your goal is to train a Large Language Model only on European Portuguese. Where do you start? What datasets are out there? What websites are being scraped for the large black box? &lt;a href="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco"&gt;Bagaço&lt;/a&gt; - named after the popular Portuguese moonshine - is a small step in that direction. &lt;/p&gt;
&lt;p&gt;&lt;img alt="Domain scatter" src="https://duarteocarmo.com/images/96/domain_scatter.webp" /&gt;&lt;/p&gt;
&lt;p&gt;In June …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Let's say your goal is to train a Large Language Model only on European Portuguese. Where do you start? What datasets are out there? What websites are being scraped for the large black box? &lt;a href="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco"&gt;Bagaço&lt;/a&gt; - named after the popular Portuguese moonshine - is a small step in that direction. &lt;/p&gt;
&lt;p&gt;&lt;img alt="Domain scatter" src="https://duarteocarmo.com/images/96/domain_scatter.webp" /&gt;&lt;/p&gt;
&lt;p&gt;In June '25, the Hugging Face team released &lt;a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb-2#%F0%9F%A5%82-fineweb2"&gt;FineWeb2&lt;/a&gt;. FineWeb2 succeeded &lt;a href="https://huggingface.co/datasets/HuggingFaceFW/fineweb"&gt;FineWeb&lt;/a&gt;, a massive cleaned up dataset of the entire internet. Think millions of web pages, specifically gathered to train Large Language Models. FineWeb2 expanded FineWeb, by adding documents in hundreds of other languages. 199 million of those are in Portuguese, but how many of them are really from Portugal, or written in European Portuguese? That's how &lt;a href="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco"&gt;Bagaço&lt;/a&gt; was born. &lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&lt;iframe
  src="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco/embed/viewer/sample/train"
  frameborder=""
  width="100%"
  height="460px"
&gt;&lt;/iframe&gt;&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Bagaço keeps only the 16 million documents that match domains from Portugal (e.g., &lt;code&gt;.pt&lt;/code&gt;). But it doesn't stop there. The goal of Bagaço is also to tell us a bit more about the data. I built two classifiers: the first classifies every document into one of nine categories: Society, Arts, Business, Science, Sports, Lifestyle, Health, Games, News. The second attributes an &lt;em&gt;educational score&lt;/em&gt; to each document (from 0 to 5) this gives an educational "value" to each web page. &lt;a href="https://huggingface.co/HuggingFaceFW/fineweb-edu-classifier"&gt;Very inspired by the FineWeb-Edu work from the HF team&lt;/a&gt;. These were trained in a similar fashion: an LLM to annotate a large sample (with Gemini and Qwen), and a balanced Logistic Regression applied on &lt;code&gt;e5&lt;/code&gt; embeddings (more info on those &lt;a href="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco#document-classification"&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img alt="Volume and score" src="https://duarteocarmo.com/images/96/volume_educational_score.webp" /&gt;&lt;/p&gt;
&lt;p&gt;It's an interesting subset to analyze. The two most popular domains are &lt;code&gt;desporto.sapo.pt&lt;/code&gt; and &lt;code&gt;record.pt&lt;/code&gt;: sports news websites (I don't think I expected anything different from Portugal). The dataset is nicely distributed across categories, but something's a bit rotten. The average educational score of the dataset is only 0.85 out of 5 (!). &lt;/p&gt;
&lt;p&gt;With FineWeb-Edu, the Hugging Face team showed that if they &lt;a href="https://arxiv.org/abs/2406.17557"&gt;filtered a dataset for educational scores above 3.0&lt;/a&gt;, the downstream LLMs had dramatically better performance. Well, if we filter Bagaço by the same criteria, I don't think we would have much left (around 1M documents, that's &lt;em&gt;it&lt;/em&gt;). If you're interested in more stats and facts, here's a &lt;a href="https://static.marimo.app/static/feafeafe-tfm7"&gt;Marimo notebook&lt;/a&gt; with an analysis. (&lt;a href="https://huggingface.co/datasets/duarteocarmo/fineweb2-bagaco/tree/main/scripts"&gt;Here are several scripts&lt;/a&gt; to create the dataset, train/run the classifiers, or to make Bagaço into an easily queryable &lt;a href="https://duckdb.org"&gt;DuckDB&lt;/a&gt; database)&lt;/p&gt;
&lt;p&gt;&lt;img alt="Volume and score" src="https://duarteocarmo.com/images/96/category_score_distribution.webp" /&gt;&lt;/p&gt;
&lt;p&gt;What if we just ignored the fact that the educational score for Bagaço is pretty low and pretrained an LLM on it? (For the record, I did - it wasn't good). Bagaço has something like ~7 Billion words, something like ~9 Billion tokens of text. According to &lt;a href="https://arxiv.org/abs/2203.15556"&gt;Chinchilla&lt;/a&gt; (~20 tokens per parameter), we could train an LLM of ~450 million parameters. Not an Opus competitor by any means - could still be an interesting model. &lt;/p&gt;
&lt;p&gt;You see, there's still much to do in this space. Most high quality data ablations have been focused on the English language. And sure – there's a lot of English out there – but there's surely a lot of European Portuguese data out there too! We just have to find it! We probably can't get away with just filtering a dataset by &lt;code&gt;.pt&lt;/code&gt; domains. We might need something more sophisticated. But data is not the only bottleneck, we also need better &lt;a href="https://duarteocarmo.com/blog/a-benchmark-for-language-models-on-european-portuguese"&gt;evaluations&lt;/a&gt; for European Portuguese. &lt;/p&gt;
&lt;p&gt;Bagaço is only the first try. The potential is huge! Time to get to work. &lt;/p&gt;</content><category term="blog"/></entry><entry><title>Retrospectiva #4</title><link href="https://duarteocarmo.com/blog/retrospectiva-4.html" rel="alternate"/><published>2026-01-28T00:00:00+01:00</published><updated>2026-01-28T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-01-28:/blog/retrospectiva-4.html</id><summary type="html">&lt;p&gt;&lt;img alt="Adriatic coast" src="https://duarteocarmo.com/images/95/cover.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Well—that was quick. Just like that, the first month of the year is gone. Winter blues would normally peak around this time. But not this year. We're spending time with family in my favorite place on Earth, somewhere along the Adriatic coast of Italy, in the &lt;a href="https://en.wikipedia.org/wiki/Marche"&gt;Marche&lt;/a&gt; region.&lt;/p&gt;
&lt;p&gt;It's …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img alt="Adriatic coast" src="https://duarteocarmo.com/images/95/cover.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Well—that was quick. Just like that, the first month of the year is gone. Winter blues would normally peak around this time. But not this year. We're spending time with family in my favorite place on Earth, somewhere along the Adriatic coast of Italy, in the &lt;a href="https://en.wikipedia.org/wiki/Marche"&gt;Marche&lt;/a&gt; region.&lt;/p&gt;
&lt;p&gt;It's hard to give into winter blues when you get to run every day around the beautiful hills of the &lt;a href="https://www.parcodelconero.org/"&gt;Conero Regional Park&lt;/a&gt;. If you read this, don't tell anyone please: let them visit the usual suspects instead.&lt;/p&gt;
&lt;p&gt;Here, life moves slow. You &lt;em&gt;enjoy&lt;/em&gt; things. You &lt;em&gt;savor&lt;/em&gt; things. You talk loud. You socialize. You live for 100 years. It's Italy.&lt;/p&gt;
&lt;h2 id="using"&gt;Using&lt;/h2&gt;
&lt;p&gt;I would be lying if I told you I haven't been using a &lt;a href="https://duarteocarmo.com/blog/how-to-police-your-agents"&gt;bunch of agents&lt;/a&gt;. I've been using LLM based tools for the better part of 2025. But something clicked towards the end, and I haven't looked back since. As for my post on &lt;a href="https://duarteocarmo.com/blog/how-to-police-your-agents"&gt;policing agents&lt;/a&gt;, my fear is still the same. That &lt;em&gt;quantity&lt;/em&gt; and &lt;em&gt;quality&lt;/em&gt; are inversely proportional. All I can do is find out ways of keeping quality high. It's nice to fire them off to do work I'm not interested in doing. Like redesigning this website for the Nth time. Or correcting typos in all my blog posts since the beginning of time.&lt;/p&gt;
&lt;p&gt;On another note. We always travel light, &lt;em&gt;even&lt;/em&gt; with a newborn. This means I have to pack just enough things - especially running gear. The &lt;a href="https://www.newbalance.com/pd/fuelcell-rebel-v5/MFCXV5-50690.html#dwvar_MFCXV5-50690_style=MFCXLV5&amp;amp;dwvar_MFCXV5-50690_width=D&amp;amp;pid=MFCXV5-50690&amp;amp;quantity=1"&gt;FuelCell Rebel v5&lt;/a&gt; have been a pleasant surprise. I'm not loyal to brands but this model won me over. They slip where it's muddy though, so beware.&lt;/p&gt;
&lt;h2 id="reading"&gt;Reading&lt;/h2&gt;
&lt;p&gt;Read more short form than long form this month. I continue going through the &lt;a href="https://huggingface.co/spaces/HuggingFaceTB/smol-training-playbook"&gt;fabulous long form writing&lt;/a&gt; from the Huggingface team. Lots of small tips and tricks, and scars on how to train Large Language models. Elon is a divisive character, but one thing I do appreciate is the opportunity of reading through the &lt;a href="https://github.com/xai-org/x-algorithm"&gt;recommendation algorithm&lt;/a&gt; that powers X.com in my free time. Finally, the GPTZero folks have written this great "&lt;a href="https://gptzero.me/news/neurips/"&gt;GPTZero finds 100 new hallucinations in NeurIPS 2025 accepted papers&lt;/a&gt;" piece about hallucinated references in NeurIPS papers. The world is getting filled with AI generated content.&lt;/p&gt;
&lt;p&gt;&lt;img alt="FineWeb chart" src="https://duarteocarmo.com/images/95/fineweb-chart.png" /&gt;&lt;/p&gt;
&lt;figcaption&gt;From &lt;a href="https://huggingface.co/spaces/HuggingFaceFW/blogpost-fineweb-v1"&gt;🍷 FineWeb: decanting the web for the finest text data at scale&lt;/a&gt;&lt;/figcaption&gt;

&lt;p&gt;I'm still finishing up both books I talked about in the last issue: &lt;a href="https://en.wikipedia.org/wiki/Old_Man%27s_War"&gt;Old Man's War&lt;/a&gt; from John Scalzi and &lt;a href="https://www.oreilly.com/library/view/llmops/9781098154196/"&gt;LLMOps&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="listening"&gt;Listening&lt;/h2&gt;
&lt;p&gt;I follow a lot of DJs on SoundCloud. One that has consistently released good mixes is &lt;a href="https://www.instagram.com/sashamvrie/"&gt;Sasha Marie&lt;/a&gt;. It's the best soundtrack for doing deep work. Whatever it is you're doing, you'll do it better when this is in the background:&lt;/p&gt;
&lt;iframe width="100%" height="300" scrolling="no" frameborder="no" allow="autoplay" src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/soundcloud%253Atracks%253A2251134248&amp;color=%23ff5500&amp;auto_play=false&amp;hide_related=false&amp;show_comments=true&amp;show_user=true&amp;show_reposts=false&amp;show_teaser=true&amp;visual=true"&gt;&lt;/iframe&gt;
&lt;div style="font-size: 10px; color: #cccccc;line-break: anywhere;word-break: normal;overflow: hidden;white-space: nowrap;text-overflow: ellipsis; font-family: Interstate,Lucida Grande,Lucida Sans Unicode,Lucida Sans,Garuda,Verdana,Tahoma,sans-serif;font-weight: 100;"&gt;&lt;a href="https://soundcloud.com/sashamarieradio" title="SASHA MARIE RADIO" target="_blank" style="color: #cccccc; text-decoration: none;"&gt;SASHA MARIE RADIO&lt;/a&gt; · &lt;a href="https://soundcloud.com/sashamarieradio/chapter-134" title="Chapter #134" target="_blank" style="color: #cccccc; text-decoration: none;"&gt;Chapter #134&lt;/a&gt;&lt;/div&gt;

&lt;p&gt;Two other recommendations that have been on repeat this month. A bit of &lt;em&gt;modern&lt;/em&gt;, and a bit of smooth.&lt;/p&gt;
&lt;iframe data-testid="embed-iframe" style="border-radius:12px" src="https://open.spotify.com/embed/track/3PJV3HC2LogvCrS6hDs2el?utm_source=generator&amp;theme=0" width="100%" height="152" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"&gt;&lt;/iframe&gt;
&lt;iframe data-testid="embed-iframe" style="border-radius:12px" src="https://open.spotify.com/embed/track/2C5M5oQOLjaTiBAInYxEty?utm_source=generator&amp;theme=0" width="100%" height="152" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"&gt;&lt;/iframe&gt;

&lt;p&gt;On another note, two podcast recommendations this month:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=NWZZEa9BURw"&gt;This interview from The Knowledge Project Podcast with Morgan Housel&lt;/a&gt;: I love how Morgan thinks, talks, and writes. His books are gold - and no wonder he's sold so many. He just resonates.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=xoynR-hWNZY"&gt;State of Agentic Coding with Armin and Ben&lt;/a&gt;: &lt;a href="https://lucumr.pocoo.org/"&gt;Armin Ronacher&lt;/a&gt; is one of those people at the bleeding edge of agentic workflows. Always interesting to understand how he's thinking.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="watching"&gt;Watching&lt;/h2&gt;
&lt;p&gt;While in Portugal, Vitto and I binged &lt;a href="https://www.rottentomatoes.com/tv/the_pitt"&gt;The Pitt&lt;/a&gt; on HBO Max. It's like Grey's Anatomy, on steroids, at 4x speed, and 5x the intensity, with a lot less drama. Or at least the soap opera one. We haven't finished it, but definitely recommend it.&lt;/p&gt;
&lt;iframe width="100%" height="315" src="https://www.youtube.com/embed/ufR_08V38sQ?si=vG6TXcfs7fsWJwW_&amp;amp;controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;

&lt;p&gt;Since we're in Italy, and it's Australian Open time, we've been watching a good amount of that as well. Tennis skyrocketed here in popularity due to Sinner, Musetti, and Paolini. So when Tennis is being played, Italy stops. But it's not only sports. We also watch intellectually rewarding things. &lt;a href="https://en.wikipedia.org/wiki/Affari_tuoi"&gt;But not this month&lt;/a&gt;.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>How to police your agents</title><link href="https://duarteocarmo.com/blog/how-to-police-your-agents.html" rel="alternate"/><published>2026-01-23T00:00:00+01:00</published><updated>2026-01-23T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-01-23:/blog/how-to-police-your-agents.html</id><summary type="html">&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/94/police-agents.jpg" alt="Monahan the Police Chief" style="max-width:100%;border-radius: 2px;"&gt;&lt;/p&gt;
&lt;p&gt;Let's face it. It &lt;em&gt;might just be&lt;/em&gt; the year of agents. If you work in tech and your workflow hasn't changed in the last year or so - you're probably doing something wrong. For those of you who have. It's fun. We're building more than ever before!&lt;/p&gt;
&lt;p&gt;But there's a dark …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/94/police-agents.jpg" alt="Monahan the Police Chief" style="max-width:100%;border-radius: 2px;"&gt;&lt;/p&gt;
&lt;p&gt;Let's face it. It &lt;em&gt;might just be&lt;/em&gt; the year of agents. If you work in tech and your workflow hasn't changed in the last year or so - you're probably doing something wrong. For those of you who have. It's fun. We're building more than ever before!&lt;/p&gt;
&lt;p&gt;But there's a dark cloud in the sky. It's the pile of trash, it's the amount of unmaintainable complexity we're putting out there. Let's get the record straight — if you're writing something to throw away — you shouldn't care about any of this, but the ones that do this professionally probably should. At the end of the day - we'll have to stick around, maintain it, make sure it performs, make sure it doesn't break in unexpected ways.&lt;/p&gt;
&lt;p&gt;Either it's a training run, a large ML pipeline, a search eval. We need to guarantee the &lt;em&gt;quality&lt;/em&gt; of what we're shipping: We need to police our agents.&lt;/p&gt;
&lt;p&gt;Now how do you do that? I do it in three ways.&lt;/p&gt;
&lt;h2 id="the-constitution"&gt;The Constitution&lt;/h2&gt;
&lt;p&gt;My global &lt;code&gt;AGENTS.md&lt;/code&gt; &lt;sup id="sf-how-to-police-your-agents-1-back"&gt;&lt;a href="#sf-how-to-police-your-agents-1" class="simple-footnote" title="Or CLAUDE.md, it doesn't matter. They are all symlinked. Claude Code, OpenCode, Codex, or even pi, they all use the same set of rules."&gt;1&lt;/a&gt;&lt;/sup&gt; is where I define the most important laws I want my agents to follow. These are closely related to my personality and taste. It's a file with scars of everything I've seen wrong from the start of my career.&lt;/p&gt;
&lt;p&gt;The file has 55 lines of Markdown and grows daily. "About Your User," "Your Behavior," "Principles," "Code Search," or "Languages" are just some of the sections it has. The idea behind it is simple: whatever I’m working on should adhere to my way of working. Also, I'm peculiar about how I want my agents to interact with me. For example, I don't want it to speak Chinese to me, I don’t want it to assume I know things I know nothing about.&lt;/p&gt;
&lt;p&gt;Here are some highlights from my &lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;...

&lt;span class="gu"&gt;## Documentation and plans&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;If docs were touched, make sure you update them with latest changes. You can run some quick ripgreps with md files to confirm if needed.
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;For docs: Be concise and durable - point to source code for specifics rather than hardcoding values that will get out of sync
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Every time the user starts repeating something very specific about a project, consider adding a rule to the AGENTS.md or CLAUDE.md of that project if one exists.
...

&lt;span class="gu"&gt;## Languages&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;I understand portuguese, french, italian, spanish, and english. Everything else, you should show the original AND the translation
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Outputing in any other language is useless, and show come with a corresponding translation

&lt;span class="gu"&gt;## Principles&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Every line of code is a liability - we should strive to make our code simple and concise
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;I prefer my functions to be small in interface but long in functionality
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;We don't MOCK in tests. We use real data and real APIs. I absolutely hate mocking
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It's a series of &lt;em&gt;very&lt;/em&gt; specific rules I want my agents to respect. You might like a different set of rules — and that's fine — I don't expect us to like the same things. But I don't believe you don't have preferences. So make sure to create one as well.&lt;/p&gt;
&lt;p&gt;This file is the Constitution. And the rules of the Constitution apply to anything I'm doing. But every project has &lt;em&gt;quirks&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="the-rules-of-the-game"&gt;The rules of the game&lt;/h2&gt;
&lt;p&gt;Every project has something specific and annoying. More likely if you're working with a team. They can be anything: A weird commit convention, a particular way of running tests, a specific server that needs to be run, an unconventional way of making a release.&lt;/p&gt;
&lt;p&gt;Your project's &lt;code&gt;AGENTS.md&lt;/code&gt; defines the rules of the game. That's where you inform the agents about the quirks of the project. A cool little trick I use is to prompt my global &lt;code&gt;AGENTS.md&lt;/code&gt; file to update my project level &lt;code&gt;AGENTS.md&lt;/code&gt; file if anything I am doing is very repetitive.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;... Global AGENTS.md
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Every time the user starts repeating something very specific about a project, consider adding a rule to the AGENTS.md or CLAUDE.md of that project if one exists.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here's an example snippet of a real world project &lt;code&gt;AGENTS.md&lt;/code&gt;. This one is for a Machine Learning pipeline built using AWS CDK:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;...Local AGENTS.md
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;For database queries, when the user is asking questions (not for code implementations), use &lt;span class="sb"&gt;`aws rds-data execute-statement`&lt;/span&gt; with the Aurora resource ARN and Secrets Manager secret ARN.
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Code artifacts should be placed in ./code_artifacts/ directory
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Scripts should be placed in the ./scripts/ directory and should be uv scripts (https://docs.astral.sh/uv/guides/scripts/)
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;If name has an acronym, like &lt;span class="sb"&gt;`ML`&lt;/span&gt;, &lt;span class="sb"&gt;`OIDC`&lt;/span&gt; only capitalise the first letter, e.g. &lt;span class="sb"&gt;`Ml`&lt;/span&gt;, &lt;span class="sb"&gt;`Oidc`&lt;/span&gt;.
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Adhere to CDK Best Practices https://docs.aws.amazon.com/cdk/v2/guide/best-practices.html
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Ensure comments are added only when they are natural and consistent with the rest of the file
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Include defensive checks or try/catch blocks only when they align with the norms of that area of the codebase (especially if not called by trusted / validated codepaths)
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Maintain consistency with the style used throughout the file
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Add comments when we are doing something non-obvious or complex, not to separate every line of code
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Note: You might also consider using something like &lt;a href="https://code.claude.com/docs/en/skills"&gt;Skills&lt;/a&gt; to solve this level of policing. Some people prefer them. I use a mix.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This ensures that whoever is building here, will generally follow the same guidelines. And these rules can go a long way to making your life easier — but they still don't &lt;em&gt;enforce&lt;/em&gt; anything — for that, we need &lt;strong&gt;the police&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="the-police"&gt;The police&lt;/h2&gt;
&lt;p&gt;Policing is all about enforcing rules. What better way to enforce rules for code, than with static code analysis? Before agents, I used to find these annoying. But nowadays, if there are 154 line-length issues in the project, I can just spin off an agent to solve it.&lt;/p&gt;
&lt;p&gt;And so, I am &lt;em&gt;very&lt;/em&gt; picky when policing agents. Let's go through a Python example.&lt;/p&gt;
&lt;p&gt;Everything starts (as always) with a &lt;code&gt;Makefile&lt;/code&gt;. This is where I define the main commands to be run. I always include a &lt;code&gt;format&lt;/code&gt; and a &lt;code&gt;check&lt;/code&gt; command. Remember, we are policing, we are picky, we are annoying: ruff, Pyright, deptry, we run it all.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nf"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;
&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Format the codebase with ruff&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;lock&lt;span class="w"&gt; &lt;/span&gt;--check
&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;ruff&lt;span class="w"&gt; &lt;/span&gt;check&lt;span class="w"&gt; &lt;/span&gt;.&lt;span class="w"&gt; &lt;/span&gt;--fix
&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;ruff&lt;span class="w"&gt; &lt;/span&gt;format&lt;span class="w"&gt; &lt;/span&gt;.

&lt;span class="nf"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;
&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# Run linting and check&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;lock&lt;span class="w"&gt; &lt;/span&gt;--check
&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;ruff&lt;span class="w"&gt; &lt;/span&gt;format&lt;span class="w"&gt; &lt;/span&gt;--check&lt;span class="w"&gt; &lt;/span&gt;.
&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;ruff&lt;span class="w"&gt; &lt;/span&gt;check&lt;span class="w"&gt; &lt;/span&gt;.
&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;pyright
&lt;span class="w"&gt; &lt;/span&gt;uv&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;deptry&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We are so annoying, that we enforce more rules that we would enforce to ourselves. So, for example, for ruff, we extend the rules to avoid our agent dropping a sneaky TODO comment instead of implementing something that should be there.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[tool.ruff.lint]&lt;/span&gt;
&lt;span class="n"&gt;extend-select&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="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"F"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# Pyflakes rules&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"W"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# PyCodeStyle warnings&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"E"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# PyCodeStyle errors&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"I"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# Sort imports properly&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"UP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Warn if certain things can changed due to newer Python versions&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"C4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Catch incorrect use of comprehensions, dict, list, etc&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"FA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Enforce from __future__ import annotations&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"ISC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Good use of string concatenation&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"ICN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Use common import conventions&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"RET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Good return practices&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"SIM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Common simplification rules&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"TID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Some good import practices&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"TC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Enforce importing certain types in a TYPE_CHECKING block&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"PTH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Use pathlib instead of os.path&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"TD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Be diligent with TODO comments&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;"NPY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# Some numpy-specific things&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Note: Shamelessly stolen from reddit user TheBB&lt;/span&gt;

&lt;span class="c1"&gt;# https://www.reddit.com/r/Python/comments/1kttfst/ruff_users_what_rules_are_using_and_what_are_you/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It's a nice, programmatic, and deterministic way of enforcing rules without having to write each one into an &lt;code&gt;AGENTS.md&lt;/code&gt; and hope the agent follows them. With that, every time the agent writes some code, it runs &lt;code&gt;make format&lt;/code&gt; and then &lt;code&gt;make check&lt;/code&gt;. If anything fails, it fixes it.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;We are becoming faster and faster at producing software. By now, that’s not &lt;em&gt;a possibility&lt;/em&gt;, that’s a fact. But the problem was never writing the code. It was always about two things: (1) knowing what code &lt;em&gt;not&lt;/em&gt; to write and (2) managing complexity. And what better way of managing complexity than by diligently &lt;em&gt;policing&lt;/em&gt; your agents?&lt;/p&gt;
&lt;p&gt;Vibe, but not too close to the sun.&lt;/p&gt;
&lt;hr&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-how-to-police-your-agents-1"&gt;Or &lt;code&gt;CLAUDE.md&lt;/code&gt;, it doesn't matter. They are all symlinked. &lt;a href="https://claude.ai/code"&gt;Claude Code&lt;/a&gt;, &lt;a href="https://opencode.ai/"&gt;OpenCode&lt;/a&gt;, &lt;a href="https://developers.openai.com/codex/cli/"&gt;Codex&lt;/a&gt;, or even &lt;a href="https://shittycodingagent.ai/"&gt;pi&lt;/a&gt;, they all use the same set of rules. &lt;a href="#sf-how-to-police-your-agents-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>Limpa: Ad-Free podcasts powered by LLMs</title><link href="https://duarteocarmo.com/blog/limpa-ad-free-podcasts-powered-by-llms.html" rel="alternate"/><published>2026-01-05T00:00:00+01:00</published><updated>2026-01-05T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2026-01-05:/blog/limpa-ad-free-podcasts-powered-by-llms.html</id><summary type="html">&lt;p&gt;I get up feeling sleepy. I lace up my running shoes and head out the door. I fire up my favourite podcast. "This show is brought to you by..." I &lt;em&gt;hate&lt;/em&gt; ads.&lt;/p&gt;
&lt;p&gt;I understand the attention economy. Companies are capitalizing more and more on everyone's time. I have nothing against …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I get up feeling sleepy. I lace up my running shoes and head out the door. I fire up my favourite podcast. "This show is brought to you by..." I &lt;em&gt;hate&lt;/em&gt; ads.&lt;/p&gt;
&lt;p&gt;I understand the attention economy. Companies are capitalizing more and more on everyone's time. I have nothing against it. But I'm also a big fan of protecting my time. A &lt;a href="https://pi-hole.net/"&gt;Pi-hole&lt;/a&gt; under my desk, &lt;a href="https://ublockorigin.com/"&gt;uBlock Origin&lt;/a&gt;, &lt;a href="https://sponsor.ajay.app/"&gt;SponsorBlock&lt;/a&gt;, I run them all. My time is mine, unless I tell you otherwise.&lt;/p&gt;
&lt;p&gt;And I spend a lot of time listening to Podcasts. And what are podcasts at the end of the day? Just mp3 files. Once you download them from the server, they're yours.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href="https://github.com/duarteocarmo/limpa"&gt;Limpa&lt;/a&gt;&lt;sup id="sf-limpa-ad-free-podcasts-powered-by-llms-1-back"&gt;&lt;a href="#sf-limpa-ad-free-podcasts-powered-by-llms-1" class="simple-footnote" title='It means "clean" in Portuguese'&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/duarteocarmo/limpa" target="_blank"&gt;&lt;/a&gt;&lt;/p&gt;&lt;a href="https://github.com/duarteocarmo/limpa" target="_blank"&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/93/limpa_screenshot.png" alt="Limpa screenshot" style="max-width: 100%; border-radius: 2px"&gt;
&lt;/center&gt;
&lt;/a&gt;&lt;p&gt;&lt;a href="https://github.com/duarteocarmo/limpa" target="_blank"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Limpa is a simple yet powerful web app. You paste in the &lt;a href="https://rss.com/tools/find-my-feed/"&gt;RSS feed&lt;/a&gt; of your favourite podcast, and it will give you back an ad-free feed that you can plug into your favourite Podcast app.&lt;/p&gt;
&lt;p&gt;It's a simple &lt;a href="https://www.djangoproject.com/"&gt;Django&lt;/a&gt; app with &lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt; on the front-end. A stack I love since it has few moving pieces. Every time a podcast gets added, I transcribe the latest episode using &lt;a href="https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3"&gt;NVIDIA's Parakeet v3&lt;/a&gt; running on &lt;a href="https://modal.com/"&gt;Modal&lt;/a&gt;. Once I extract the &lt;a href="https://github.com/duarteocarmo/limpa/blob/master/limpa/services/modal_transcription.py#L72"&gt;transcript with timestamps&lt;/a&gt;, I can &lt;a href="https://github.com/duarteocarmo/limpa/blob/master/limpa/services/extract.py#L48"&gt;prompt an LLM&lt;/a&gt; to get the exact timestamps of the ads that should be cut out. I then pass those to &lt;a href="https://www.ffmpeg.org/"&gt;ffmpeg&lt;/a&gt; and voilà: ad-free podcasts.&lt;/p&gt;
&lt;p&gt;Django's new &lt;a href="https://docs.djangoproject.com/en/6.0/topics/tasks/"&gt;Tasks framework&lt;/a&gt; has also been a joy to use. No more &lt;a href="https://docs.celeryq.dev/en/v5.5.3/django/first-steps-with-django.html"&gt;Celery&lt;/a&gt;, no more &lt;a href="https://flower.readthedocs.io/en/latest/"&gt;Flower&lt;/a&gt;, just a simple database to run background tasks. The fewer moving pieces, the fewer mistakes &lt;a href="https://opencode.ai/"&gt;my agents&lt;/a&gt; are prone to making.&lt;/p&gt;
&lt;p&gt;If you're wondering - no - I'm not planning on hosting this service to others. I'm not interested in discussing the nuances and implications of providing this as a service to others.&lt;sup id="sf-limpa-ad-free-podcasts-powered-by-llms-2-back"&gt;&lt;a href="#sf-limpa-ad-free-podcasts-powered-by-llms-2" class="simple-footnote" title="That might be illegal, and you probably shouldn't do it"&gt;2&lt;/a&gt;&lt;/sup&gt;. The project's simple and &lt;a href="https://github.com/duarteocarmo/limpa"&gt;well documented&lt;/a&gt;, and so it should be easy for you to run it for yourself.&lt;/p&gt;
&lt;p&gt;Feel free to fork, and build on top of it!&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-limpa-ad-free-podcasts-powered-by-llms-1"&gt;It means "clean" in Portuguese &lt;a href="#sf-limpa-ad-free-podcasts-powered-by-llms-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-limpa-ad-free-podcasts-powered-by-llms-2"&gt;That might be illegal, and you probably shouldn't do it &lt;a href="#sf-limpa-ad-free-podcasts-powered-by-llms-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>Retrospectiva #3</title><link href="https://duarteocarmo.com/blog/retrospectiva-3.html" rel="alternate"/><published>2025-12-25T00:00:00+01:00</published><updated>2025-12-25T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-12-25:/blog/retrospectiva-3.html</id><summary type="html">&lt;p&gt;And just like that, it's the end of the year again. We did not expect to make it to the South for Christmas this year. Getting a passport for a newborn is a painful, bureaucracy-filled process, especially for a Portuguese-Italian baby born in Denmark. But somehow—magically—the Italian embassy …&lt;/p&gt;</summary><content type="html">&lt;p&gt;And just like that, it's the end of the year again. We did not expect to make it to the South for Christmas this year. Getting a passport for a newborn is a painful, bureaucracy-filled process, especially for a Portuguese-Italian baby born in Denmark. But somehow—magically—the Italian embassy came through in 2 days (looking at you and your 4-month wait, Portugal). And so we flew down and surprised everyone.&lt;/p&gt;
&lt;p&gt;Lisbon. It's loud, dirty, &lt;a href="https://www.theguardian.com/commentisfree/2025/jun/25/lisbon-europe-portugal-golden-visa-capital-investors-short-term-rentals"&gt;well-known for all the wrong reasons&lt;/a&gt;, but it's &lt;em&gt;home&lt;/em&gt;. I couldn't be happier to bring Allegra down to spend her first Christmas with the family.&lt;/p&gt;
&lt;p&gt;It's the end of the year, before we get to the &lt;em&gt;regularly scheduled&lt;/em&gt; Retrospectiva, let's talk a bit about 2025.&lt;/p&gt;
&lt;h2 id="2025-look-back"&gt;2025 look back&lt;/h2&gt;
&lt;p&gt;2025 was an eventful year. On the personal side, one of the most eventful years in a while. We got married and managed to put everyone we love in a single place. We travelled just the right amount. I've read just the right amount of books - and started reading more fiction.&lt;/p&gt;
&lt;p&gt;As for running, it was a transition year. I used to run two marathons every year. Recently, I decided to tone that down to just one. This year I ran the &lt;a href="https://oslomaraton.no/en/"&gt;Oslo marathon&lt;/a&gt;, and a half-marathon across the &lt;a href="https://brolobet2025.dk/"&gt;Øresund&lt;/a&gt; bridge. No clean training block for neither of them. I did try to keep my mileage relatively high. As I'm writing this, I've ran 1937 km this year. Pretty close to my 2000 km target for the year. Maybe I can still make it.&lt;/p&gt;
&lt;p&gt;2025 was &lt;em&gt;also&lt;/em&gt; a challenging year for our family. I won't get into details. A lot of unexpected things happened. Thankfully, we all managed to pull through. The year finished with a bang and 'Legra is here with us now.&lt;/p&gt;
&lt;p&gt;In what regard work, the year was busy. The stakes were higher, and more was expected. Larger scale, more challenging problems, and lots of new problems. Two main themes that I'm noticing. The first is that LLM-backed applications are growing more than ever - but most don't really know how to measure and improve them. The second is the &lt;a href="https://x.com/duarteocarmo/status/2001566862182879738"&gt;great cleanup&lt;/a&gt; - which is a topic for a future post.&lt;/p&gt;
&lt;p&gt;Now, in the end of December, we take a deep breath - stop to think - and put some goals in the board for 2026.&lt;/p&gt;
&lt;h2 id="using"&gt;Using&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Oura ring&lt;/strong&gt; (&lt;a href="https://ouraring.com/store/rings"&gt;link&lt;/a&gt;): For years I've been a happy Garmin user. 10-day battery life, great for tracking my running. But there's one thing that always annoyed me: sleep tracking. If I wake up at 4 AM and get back to bed, it doesn't mean I slept 5 hours. Also, Garmin has a &lt;em&gt;passive&lt;/em&gt; approach to giving me insights. I can go and browse the graphs - but they won't proactively tell me how I'm doing. I hate subscriptions. The &lt;a href="https://us.amazfit.com/products/helio-strap"&gt;Helio Strap&lt;/a&gt; was good. But still passive - so I sold it. The Oura &lt;em&gt;delivers&lt;/em&gt;. The app is well designed, the sleep tracking is accurate, and the insights are actually actionable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bimby TM7&lt;/strong&gt; (&lt;a href="https://www.vorwerk.com/pt/pt/c/home/produtos/bimby/tm7"&gt;link&lt;/a&gt;): I used to cook the &lt;em&gt;exact same&lt;/em&gt; pasta dish every single day. Food is important - I just never cared. Cooking was just always too abstract. A "spoon" of this, a "pinch" of that - what the hell do you mean? The Bimby has removed any and all friction in getting an actual dish ready. You can browse through thousands of recipes and I don't need to think, I can just follow the instructions. The iOS app is also great - and warns me when I need to come back to it and do something. Its stupid expensive - but we are incredibly happy with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OpenCode&lt;/strong&gt; (&lt;a href="https://opencode.ai/"&gt;link&lt;/a&gt;): I've told you before: at this pace of change my editing workflow changes almost every week. In the past few weeks I've caught myself ditching Claude Code and Codex more and more for OpenCode. The terminal UI is beautifully. It's &lt;a href="https://github.com/sst/opencode"&gt;100% open source&lt;/a&gt;. It allows me to use any model I want. This means any &lt;a href="https://openrouter.ai/"&gt;OpenRouter&lt;/a&gt; model, or - even better - any model already included in my GitHub Copilot plan. I don't need another subscription. Especially for something closed source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Neovim Pack&lt;/strong&gt; (&lt;a href="https://neovim.io/doc/user/pack.html"&gt;link&lt;/a&gt;): I live mostly in the terminal. Not because I think it's cool. But because that's where I'm more productive. For the past years I've used &lt;a href="https://www.lazyvim.org/"&gt;LazyVim&lt;/a&gt; for &lt;a href="https://github.com/duarteocarmo/dotfiles/tree/master/.config/nvim"&gt;my config&lt;/a&gt;, but something started to feel bloated. Too many things going on, too many bells and whistles I don't need. I want to get in, change, get out. Neovim now has a &lt;a href="https://neovim.io/doc/user/pack.html"&gt;native package manager,&lt;/a&gt; and I've started building my own minimal config with it (currently &lt;a href="https://github.com/duarteocarmo/dotfiles/blob/master/.config/nvim-minimal/init.lua"&gt;100 lines of Lua&lt;/a&gt;- and work in progress).&lt;/p&gt;
&lt;h2 id="reading"&gt;Reading&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Old Man's War - John Scalzi&lt;/strong&gt; (&lt;a href="https://www.goodreads.com/book/show/36510196-old-man-s-war"&gt;link&lt;/a&gt;): Still working my way through this one. I knew Scalzi wouldn't let me down. I love his direct, no-bs writing style. Nothing beats ending the day with a book set somewhere very, &lt;em&gt;very&lt;/em&gt; faraway. There are some interesting parallels to our world right now. Everyone's got their own personal AI assistant (which the main character nicknames "Asshole"), everyone has to sign a user agreement to use their new body. It almost feels like non-fiction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LLMOps - Abi Aryan&lt;/strong&gt; (&lt;a href="https://www.amazon.com/LLMOps-Managing-Language-Models-Production/dp/1098154207#customerReviews"&gt;link&lt;/a&gt;): My technical read for the holiday season, the next on my technical book backlog. I've been a reader of Abi's &lt;a href="https://modelcraft.substack.com/"&gt;ModelCraft&lt;/a&gt; newsletter, and have high hopes for this one.&lt;/p&gt;
&lt;h2 id="listening"&gt;Listening&lt;/h2&gt;
&lt;p&gt;I've been listening to a couple of different music "areas". I don't think we can call them genres.&lt;/p&gt;
&lt;p&gt;The first one is &lt;strong&gt;Italian indie alternative&lt;/strong&gt;. One of my favourite things about speaking another language is that you get to immerse yourself in the culture that speaks it, which includes - obviously - their music. I've spent the last few years exploring most of what the Italian scene has to offer, but lately it's been a mix of &lt;a href="https://open.spotify.com/artist/3r7ayqsHrVs1aeD7SiBTsz?si=g1TEFkFfQaeggjUn6R0ypg"&gt;irossa&lt;/a&gt;, some &lt;a href="https://open.spotify.com/track/3RpG5glcogbXModIh6P87R?si=94db23bf81264bc2"&gt;weird things&lt;/a&gt;, and &lt;a href="https://open.spotify.com/artist/0ErDKYNv448COBCNdnqYIm?si=BVcyo6ipTPCM97Co4Y5f_g"&gt;Marco Castello&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The second one I call &lt;strong&gt;jazz ambient&lt;/strong&gt;. When I play music (almost always), I tend to play it all over the house. Recently, both &lt;a href="https://open.spotify.com/artist/2h5syT5XdsQgKLq8Yn1klO?si=HBNSUbwURWGRA1vervExSQ"&gt;Nala Sinephro&lt;/a&gt; and &lt;a href="https://open.spotify.com/artist/2eO4klJg324zroGqnBkqk3?si=OdCnbiz4Q86HcVcXcct39g"&gt;cktrl&lt;/a&gt; have been on heavy rotation. They surely put me in the Christmas spirit.&lt;/p&gt;
&lt;div style="display: flex; flex-wrap: wrap; gap: 12px;"&gt;
  &lt;iframe style="border-radius:12px; flex:1 1 300px; min-width:0;" src="https://open.spotify.com/embed/track/3zrfPWeaPgpUZyu19BjBTf?utm_source=generator" height="352" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"&gt;&lt;/iframe&gt;
  &lt;iframe style="border-radius:12px; flex:1 1 300px; min-width:0;" src="https://open.spotify.com/embed/track/2QUXnZuZYqc5RAgSJILpb6?utm_source=generator" height="352" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"&gt;&lt;/iframe&gt;
&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
That's it for this month's Retrospectiva. See you in 2026!&lt;/p&gt;</content><category term="blog"/></entry><entry><title>From NutriBench to Taralli: How far can you take a prompt?</title><link href="https://duarteocarmo.com/blog/from-nutribench-to-taralli-how-far-can-you-take-a-prompt.html" rel="alternate"/><published>2025-12-23T00:00:00+01:00</published><updated>2025-12-23T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-12-23:/blog/from-nutribench-to-taralli-how-far-can-you-take-a-prompt.html</id><summary type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/91/experiment_results.png" alt="Taralli Screenshots" style="max-width: 100%; border-radius: 2px"&gt;
&lt;figcaption&gt;Benchmarking calorie prediction for Taralli&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;There's something very funny about the current Machine Learning and AI landscape. If you're in the field you probably heard about it. "Vibes" they call it. When someone wants to test something out, they conduct a "vibe test".&lt;/p&gt;
&lt;p&gt;I call bullshit. How are you supposed …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/91/experiment_results.png" alt="Taralli Screenshots" style="max-width: 100%; border-radius: 2px"&gt;
&lt;figcaption&gt;Benchmarking calorie prediction for Taralli&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;There's something very funny about the current Machine Learning and AI landscape. If you're in the field you probably heard about it. "Vibes" they call it. When someone wants to test something out, they conduct a "vibe test".&lt;/p&gt;
&lt;p&gt;I call bullshit. How are you supposed to improve something if you can't even measure it? How do you know you didn't just make it worse?&lt;/p&gt;
&lt;p&gt;In the past month, I've been working on &lt;a href="https://duarteocarmo.com/blog/taralli-home-cooked-food-tracking-without-the-bs"&gt;Taralli&lt;/a&gt;. And I'd like to show you how to incrementally improve an LLM based system.&lt;/p&gt;
&lt;p&gt;But first, let's take a little side quest.&lt;/p&gt;
&lt;h2 id="nutribench-evaluating-llms-on-nutrition-estimation"&gt;NutriBench: Evaluating LLMs on nutrition estimation&lt;/h2&gt;
&lt;p&gt;While browsing arXiv the other day &lt;a href="https://arxiv.org/abs/2407.12843"&gt;I stumbled upon NutriBench&lt;/a&gt;. This is exciting - I thought to myself - it looks like someone else is also looking into this problem. NutriBench is a research project launched by the University of California that looks to quantify exactly Taralli's problem: "How good are Large Language Models at estimating the nutritional content of foods?".&lt;/p&gt;
&lt;p&gt;The team created &lt;a href="https://huggingface.co/datasets/dongx1997/NutriBench/viewer/v1?views%5B%5D=v1_wweia_meal_metric"&gt;a dataset for ~12K meal descriptions&lt;/a&gt; from two data sources (&lt;a href="https://www.ars.usda.gov/northeast-area/beltsville-md-bhnrc/beltsville-human-nutrition-research-center/food-surveys-research-group/docs/wweianhanes-overview/"&gt;WWEIA&lt;/a&gt; and &lt;a href="https://www.fao.org/gift-individual-food-consumption/en/"&gt;FAO/WHO&lt;/a&gt;). Every row in the dataset is a meal description with the corresponding nutritional details (carbs, protein, fat, and energy). To answer the question, they tested 4 different prompting techniques and 12 different LLMs. The prompting techniques were the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Base&lt;/strong&gt;: A simple prompt with some few-shot examples.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chain-of-Thought (CoT)&lt;/strong&gt;: A slightly more complex prompt with some reasoning steps to "force" the model to reason.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retrieval-Augmented Generation (RAG)&lt;/strong&gt;: A system that uses a retrieval database (which they called Retri-DB), to aid the LLM in estimating carbs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAG+CoT&lt;/strong&gt;: A mix of the former two.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/duarteocarmo/e9d89bd75363be3540fcdf8074e71100"&gt;If you are curious about the prompts, click here.&lt;/a&gt;&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/91/nutribench_results.png" alt="NutriBench results" style="max-width: 100%;border-radius: 2px"&gt;
&lt;figcaption&gt;NutriBench results&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;You're probably wondering: okay, but how good are the models at this? The answer is a resounding &lt;em&gt;MEH&lt;/em&gt;. The best performer is GPT-4o with a Chain-of-Thought prompt. It responds to ~99% of prompts (models can refuse to answer) and achieves an Acc@7.5 of 66.82%&lt;sup id="sf-from-nutribench-to-taralli-how-far-can-you-take-a-prompt-1-back"&gt;&lt;a href="#sf-from-nutribench-to-taralli-how-far-can-you-take-a-prompt-1" class="simple-footnote" title="For reference, this is on par with a Human nutritionist with internet access"&gt;1&lt;/a&gt;&lt;/sup&gt;. In other words, &lt;strong&gt;its carb predictions fall within ±7.5g of the actual value about two-thirds of the time&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;What if we could use the NutriBench dataset to improve Taralli?&lt;/p&gt;
&lt;h2 id="improving-tarallis-nutritional-estimation"&gt;Improving Taralli's nutritional estimation&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/duarteocarmo/58fda6d982fb6d9dd5bfb561214a77ce"&gt;Here's the notebook with the entire process&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The first thing, as always, is to create good dataset. I created a new dataset of 107 examples of food descriptions and their corresponding total calories&lt;sup id="sf-from-nutribench-to-taralli-how-far-can-you-take-a-prompt-2-back"&gt;&lt;a href="#sf-from-nutribench-to-taralli-how-far-can-you-take-a-prompt-2" class="simple-footnote" title="I could have used more, but wanted to keep things fast"&gt;2&lt;/a&gt;&lt;/sup&gt;. I used the &lt;a href="https://huggingface.co/datasets/dongx1997/NutriBench"&gt;second version of the NutriBench&lt;/a&gt; dataset on Hugging Face. I mixed in that dataset with some other examples from a previous golden dataset.&lt;/p&gt;
&lt;details&gt;
  &lt;summary&gt;Expand to see some examples of the dataset&lt;/summary&gt;
  &lt;pre&gt;&lt;code&gt;
 [
  {
    "input": "For dinner, I'm having 25 grams of bread, 150 grams of chicken wings, and a 250-gram mixed vegetable salad.",
    "total_calories": 633.0,
    "food_groups": [
      "grain",
      "meat and alternatives",
      "vegetable"
    ],
    "source": "nutribench"
  },
  {
    "input": "I enjoyed 200 grams of tea with sugar along with 230 grams of coconut milk rice for breakfast.",
    "total_calories": 558.0,
    "food_groups": [
      "grain",
      "fruit"
    ],
    "source": "nutribench"
  },
  {
    "input": "stracciatella with confit grapes and 2 little focaccia pieces",
    "total_calories": 350.0,
    "food_groups": [
      "fruit",
      "dairy",
      "grain"
    ],
    "source": "golden_dataset"
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;

&lt;p&gt;Now I needed to design an evaluation metric. For NutriBench they used accuracy of carb prediction at ±7.5 grams. In our case, I'm more interested in the accuracy calorie prediction. One of the nice things about DSPy is that it forces you to write down your evaluation metric explicitly. Below is the evaluation metric I used: In short, we want the predicted calories to be within 10% of the ground truth calories. Which means our metric will then be Accuracy@10%.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;eval_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pred_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pred_trace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Prediction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# see notebook for more detailed metric (this is simplified)&lt;/span&gt;
    &lt;span class="n"&gt;total_calories_example&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_calories&lt;/span&gt;
    &lt;span class="n"&gt;total_calories_predicted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;na&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_calories&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;within_threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_calories_example&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;total_calories_predicted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nb"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;total_calories_example&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;within_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;feedback_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"INCORRECT: Your answer was not within 10% of the correct answer (yours: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_calories_predicted&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, correct: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_calories_example&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Prediction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feedback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;feedback_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;feedback_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"CORRECT: Your answer was within 10% of the correct answer (yours: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_calories_predicted&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, correct: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_calories_example&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Prediction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feedback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;feedback_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here are the different prompts I tested:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vanilla&lt;/strong&gt;: Zero shot prompt using the DSPy format.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bootstrap few shot golden only&lt;/strong&gt;: Using the golden dataset I previously collected. This is what was running in production for Taralli. &lt;a href="https://dspy.ai/api/optimizers/BootstrapFewShotWithRandomSearch/"&gt;BootstrapFewShotWithRandomSearch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bootstrap few mixed&lt;/strong&gt;: Uses a dataset that is a mix of my data and the NutriBench data. &lt;a href="https://dspy.ai/api/optimizers/BootstrapFewShotWithRandomSearch/"&gt;BootstrapFewShotWithRandomSearch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MIPROv2&lt;/strong&gt;: Multiprompt Instruction PRoposal Optimizer Version 2, a prompt optimizer capable of optimizing both the instructions (e.g., the prompt) and the few shot examples. &lt;a href="https://dspy.ai/api/optimizers/MIPROv2/"&gt;MIPROv2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GEPA&lt;/strong&gt;: The new kid on the block for prompt optimizers. The nice thing about GEPA is that you can also give it some textual feedback on the incorrect predictions, and it will use it. &lt;a href="https://dspy.ai/api/optimizers/GEPA/overview/"&gt;GEPA&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As for models, I chose some that were cheap and fast - some closed (Gemini) and other Open (DeepSeek). I decided to test, &lt;code&gt;Gemini 2.5 Flash&lt;/code&gt; (what I am currently using in production), &lt;code&gt;DeepSeech v3.2&lt;/code&gt; with thinking on and off, and Google's new &lt;code&gt;Gemini 3 Flash&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Some thoughts on the results:
- The &lt;strong&gt;best performing model&lt;/strong&gt; is &lt;code&gt;Gemini 3 Flash&lt;/code&gt; with a set of 16 examples in the prompt. It achieves a score of around 60%. Similar to NutriBench, although the problems are slightly different. &lt;a href="https://gist.github.com/duarteocarmo/99df3327c918daa5b68c97572a3e0ee6#file-example-prediction-best-performing-prompt"&gt;Here's an example prediction&lt;/a&gt;.
- The &lt;strong&gt;GEPA optimization&lt;/strong&gt; came up &lt;a href="https://gist.github.com/duarteocarmo/99df3327c918daa5b68c97572a3e0ee6#file-gepa-example"&gt;with a prompt itself&lt;/a&gt;. When using that prompt with &lt;code&gt;Gemini 2.5 Flash&lt;/code&gt; it performs respectably well. However, the GEPA prompt actually fails to provide the correct response format when used with any other model. In other words, the prompt was &lt;strong&gt;overfit&lt;/strong&gt; to the model it was trained on. As a result, you only see one GEPA score in the results graph.
- The &lt;strong&gt;few-shot approach&lt;/strong&gt; is the most reliable one. It is model agnostic, performs well, and follows the styles of examples in a faithful manner.&lt;/p&gt;
&lt;p&gt;So, I decided to update Taralli to use &lt;code&gt;Gemini 3 Flash&lt;/code&gt; with the few-shot approach. This approach is ~15% more accurate when compared to the old version, which was running on &lt;code&gt;Gemini 2.5 Flash&lt;/code&gt;, with the exact same optimizer. In conclusion, &lt;strong&gt;all I changed was a model string&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here's a snippet taken from the API. Since I'm using &lt;a href="https://openrouter.ai/"&gt;OpenRouter&lt;/a&gt;, I can also define our 2nd best performing model as a back up, just like so:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;LM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"openrouter/google/gemini-3-flash-preview:nitro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;extra_body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="s2"&gt;"models"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"deepseek/deepseek-v3.2:nitro"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="running-on-the-edge"&gt;Running on the edge&lt;/h2&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/91/taralli_screenshots.png" alt="Taralli experiment results" style="max-width: 100%"&gt;
&lt;figcaption&gt;Taralli's new features showcased&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;I've updated Taralli to use Apple's new Liquid Glass design for iOS 26. That was pretty simple. It's a 5 file SwitfUI app with around 4.5 MB. You can now also set periodic reminders so that you don't forget to track.&lt;/p&gt;
&lt;p&gt;One of the best new features is the ability to use Taralli &lt;em&gt;completely offline&lt;/em&gt; with an on-device LLM for nutritional analysis. The code snippet below transforms a DSPy optimized program into OpenAI-compatible messages. I created an endpoint that takes a &lt;code&gt;food_description&lt;/code&gt; and returns these messages for on-device inference. The iOS app calls this endpoint, receives the populated template, and uses it with the on-device model. As a backup, we can bundle the template directly in the app to skip the API call entirely. Long story short, Taralli now works on airplane (mode).&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_classifier_template&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;program&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_classifier&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChatAdapter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;openai_messages_format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore&lt;/span&gt;
            &lt;span class="n"&gt;demos&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;demos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;{{&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;}}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_fields&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;# type: ignore&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;program&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;named_predictors&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="s2"&gt;"self"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;openai_messages_format&lt;/span&gt;
    &lt;span class="c1"&gt;# returns [{"role": "system", "content": feajfeafl}, {"role..}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="final-summary"&gt;Final summary&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hill climbing NutriBench:&lt;/strong&gt; The best performing model/prompt combo in my experiments hit a 60% Accuracy @ 10%. The task is slightly different than NutriBench - we also predict fat, protein, and carbs for example. However, I'm still interested in exploring how we could possibly hill climb that dataset. Could we get to 90%? Should we try a larger model? More thinking? Web browsing? How much of a difference would it make?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Training a model&lt;/strong&gt;: We could obviously also train a model on this task. The prompt optimizations will only get you so far, but it would also be very interesting to see how much a fine-tuned model would perform. To be completely honest, I still believe that in order to get the best performance possible, we would probably need some sort of connection to the outside world (web search, RAG, or the likes).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Few shot wins:&lt;/strong&gt; From several experiments with Taralli, and a couple of other projects, something has become more and more obvious: few-shot often wins. Not only when it relates to accuracy, but also specifying the correct format for the output that you are looking for. If you are dealing with a complicated task - try squeezing in some good examples of model behaviour.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vibes are useless:&lt;/strong&gt; You can't improve something you can't measure. Whenever someone tells me "the vibes are off," I call bullshit. Off? By how much? Can we put a number to it? Whenever you are improving a system, try to be scientific about it. Measure first, understand how the current system is performing, try to improve it, and then measure again.&lt;/li&gt;
&lt;/ul&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-from-nutribench-to-taralli-how-far-can-you-take-a-prompt-1"&gt;For reference, this is on par with a Human nutritionist with internet access &lt;a href="#sf-from-nutribench-to-taralli-how-far-can-you-take-a-prompt-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-from-nutribench-to-taralli-how-far-can-you-take-a-prompt-2"&gt;I could have used more, but wanted to keep things fast &lt;a href="#sf-from-nutribench-to-taralli-how-far-can-you-take-a-prompt-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>Retrospectiva #2</title><link href="https://duarteocarmo.com/blog/retrospectiva-2.html" rel="alternate"/><published>2025-11-25T00:00:00+01:00</published><updated>2025-11-25T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-11-25:/blog/retrospectiva-2.html</id><summary type="html">&lt;p&gt;Big news in the state of Denmark. And no, &lt;a href="https://www.shakespeare-online.com/quickquotes/quickquotehamletdenmark.html"&gt;nothing's rotten&lt;/a&gt;. Allegra just came into the world.&lt;/p&gt;
&lt;p&gt;As she takes a nap, I take the opportunity to write November's Retrospectiva update.&lt;/p&gt;
&lt;p&gt;The most relevant thing this month is probably the release of my &lt;a href="https://duarteocarmo.com/blog/book-release-deepseek-in-practice"&gt;recent book about DeepSeek&lt;/a&gt;. It's nice to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Big news in the state of Denmark. And no, &lt;a href="https://www.shakespeare-online.com/quickquotes/quickquotehamletdenmark.html"&gt;nothing's rotten&lt;/a&gt;. Allegra just came into the world.&lt;/p&gt;
&lt;p&gt;As she takes a nap, I take the opportunity to write November's Retrospectiva update.&lt;/p&gt;
&lt;p&gt;The most relevant thing this month is probably the release of my &lt;a href="https://duarteocarmo.com/blog/book-release-deepseek-in-practice"&gt;recent book about DeepSeek&lt;/a&gt;. It's nice to see it finally come to life. I've also been getting more regular in my running, hitting a nice weekly volume more consistently, it does get harder with cold weather. Given the recent events, I'm not sure it will last. In the midst of Vitamin D tablets and diapers, we'll do, as always, the best we can.&lt;/p&gt;
&lt;h2 id="using"&gt;Using&lt;/h2&gt;
&lt;p&gt;In preparation for the baby arriving, I hunted for a pair of headphones I could wear during calls. It needed to do two things: have a microphone that blocks out any surrounding noises (you know), but at the same time allow me to be aware of my surroundings. I regularly wear the &lt;a href="https://shokz.com/pages/openrun"&gt;Open Run&lt;/a&gt; for running. I've enjoyed the bone conduction when I need to be aware of what's around me. I decided to take the plunge and get the &lt;a href="https://shokz.com/products/openmeet"&gt;OpenMeet&lt;/a&gt;'s. After a month of use, I definitely recommend them. They are niche though: they do the &lt;em&gt;exact opposite&lt;/em&gt; of cancelling the noise around you - so be aware of that.&lt;/p&gt;
&lt;p&gt;For years, I've been a fan of &lt;a href="https://jupyter.org/"&gt;Jupyter Notebooks&lt;/a&gt;. If it's more than a script, and I need to know what's going on with the data, notebooks are the way I go. After reading so much about it, I decided to finally try &lt;a href="https://marimo.io/"&gt;Marimo&lt;/a&gt; (&lt;a href="https://www.coreweave.com/news/coreweave-acquires-marimo-to-unify-the-generative-ai-developer-workflow"&gt;just acquired by Coreweave&lt;/a&gt;). The visuals and &lt;a href="https://docs.marimo.io/examples/"&gt;components&lt;/a&gt; are nice and extensive. The fact that any notebook is &lt;em&gt;just&lt;/em&gt; a uv script makes things &lt;em&gt;very&lt;/em&gt; reproducible. You can also transform notebooks into dashboards &lt;a href="https://docs.marimo.io/guides/deploying/programmatically/"&gt;and embed&lt;/a&gt; them into your FastAPI app which is great. But most of my notebooks don't need an AI panel, MCP connectors, interactive cells, and other bells and whistles. They need to show cell outputs at the bottom, a running order I can understand, Copilot, and VIM bindings. That's it.&lt;/p&gt;
&lt;p&gt;So for now, my go-to is still:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;uv&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;--with&lt;span class="w"&gt; &lt;/span&gt;jupyter&lt;span class="w"&gt; &lt;/span&gt;--with&lt;span class="w"&gt; &lt;/span&gt;jupyter_copilot&lt;span class="w"&gt; &lt;/span&gt;--with&lt;span class="w"&gt; &lt;/span&gt;jupyterlab-vim&lt;span class="w"&gt; &lt;/span&gt;jupyter&lt;span class="w"&gt; &lt;/span&gt;lab
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On a final note, I got completely rid of &lt;a href="https://www.dropbox.com"&gt;Dropbox&lt;/a&gt;. For years, it's been the tool of choice for my &lt;em&gt;important&lt;/em&gt; documents. They've been asking me to upgrade to a paid plan for years now to go beyond my 3GB limit(!). I've replaced it with self hosted &lt;a href="https://nextcloud.com/"&gt;NextCloud&lt;/a&gt; on my &lt;a href="https://duarteocarmo.com/blog/how-i-self-host-in-2024"&gt;Coolify&lt;/a&gt; instance. Now I have unlimited space, a lighter mac client, and exactly the same features. Without spending a single cent more.&lt;/p&gt;
&lt;h2 id="reading"&gt;Reading&lt;/h2&gt;
&lt;p&gt;I finally took the plunge, and have gone back to reading some good old science fiction (the last thing I got seriously into was the &lt;a href="https://www.goodreads.com/series/117100-red-rising-saga"&gt;Red Rising &lt;/a&gt;series). I've been a John Scalzi fan for a long time, so I decided to pick up &lt;a href="https://www.goodreads.com/book/show/36510196-old-man-s-war"&gt;Old Man's War&lt;/a&gt;. I like Scalzi's straight, direct, and &lt;em&gt;no-bs&lt;/em&gt; writing style. Great for when I don't have the lights on and have to rely on my &lt;a href="https://duarteocarmo.com/blog/goodbye-kindle-i-dont-think-ill-miss-you"&gt;Palma&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I'm also diving deep into LLM architectures and pre-training this month. &lt;a href="https://huggingface.co/spaces/HuggingFaceFW/blogpost-fineweb-v1"&gt;&lt;em&gt;"FineWeb: decanting the web for the finest text data at scale"&lt;/em&gt;&lt;/a&gt;, was an insightful view into the world of creating datasets for pre-training. Full of gems. One of my favorites was the in-depth explanation of the &lt;a href="https://huggingface.co/spaces/HuggingFaceFW/blogpost-fineweb-v1"&gt;FineWeb-Edu classifier&lt;/a&gt;. The idea is simple: to train a classifier we can use to filter pre-training data in order to get the most "refined" pretraining dataset possible.&lt;/p&gt;
&lt;p&gt;Highly related to that, I ordered &lt;em&gt;&lt;a href="https://www.lulu.com/shop/nouamane-tazi-and-ferdinand-mom-and-haojun-zhao-and-phuc-nguyen/the-ultra-scale-playbook/paperback/product-45yk9dj.html?page=1&amp;amp;pageSize=4"&gt;"The Ultra-Scale Playbook: Training LLMs on GPU Clusters"&lt;/a&gt;&lt;/em&gt;. It’s the second book in this "trilogy" of blog posts (3rd one still being written). It starts off with some basic concepts like Data, Tensor, and Context Parallelism, but goes off to more advanced things like training configurations and GPU kernels. You can really feel the "war stories" some of the authors went through before writing it.&lt;/p&gt;
&lt;h2 id="listening"&gt;Listening&lt;/h2&gt;
&lt;p&gt;I gave Apple Music a fair shot, but I had to go back to Spotify. Music is too important to have a search functionality that doesn't work. I don't want a single barrier between me and what I want to listen to.&lt;/p&gt;
&lt;p&gt;There are a lot of things wrong with Apple Music. But one good thing is the live radio and shows hosted on there. Particularly &lt;a href="https://music.apple.com/us/curator/classical-connections-radio-with-alexis-ffrench/1653477785"&gt;Classical Connections Radio&lt;/a&gt; which blends modern and classical music in a very original way. That show led me to the deep rabbit hole of &lt;a href="https://en.wikipedia.org/wiki/Duduk"&gt;Duduk&lt;/a&gt; based music, which is particularly useful for soothing a sleeping baby.&lt;/p&gt;
&lt;p&gt;Here's an amazing one by &lt;a href="https://en.wikipedia.org/wiki/Djivan_Gasparyan"&gt;Djivan Gasparyan&lt;/a&gt;:&lt;/p&gt;
&lt;iframe data-testid="embed-iframe" style="border-radius:12px" src="https://open.spotify.com/embed/track/74EHZDVuASkFGH5BSy9KPp?utm_source=generator&amp;theme=0" width="100%" height="152" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"&gt;&lt;/iframe&gt;

&lt;h2 id="watching"&gt;Watching&lt;/h2&gt;
&lt;p&gt;We found some time to pull the old projector out. Lately, the focus has been Spanish thrillers (or as Italians call the genre: &lt;em&gt;giallo&lt;/em&gt; - i.e., yellow). The Netflix Spain originals based on &lt;a href="https://www.goodreads.com/author/show/8339062.Javier_Castillo"&gt;Javier Castillo's&lt;/a&gt; writing have been the binge target. First up was the &lt;a href="https://www.rottentomatoes.com/tv/the_crystal_cuckoo"&gt;Crystal Cuckoo&lt;/a&gt;, which was definitely entertaining. That sparked our interest, so we started &lt;a href="https://www.rottentomatoes.com/tv/the_snow_girl"&gt;"Chica de Nieve"&lt;/a&gt; (i.e., the Snow Girl), which has been even better so far.&lt;/p&gt;
&lt;center&gt;
&lt;iframe width="100%" height="315" src="https://www.youtube.com/embed/SPcE8J2Xwjc?si=c54oLGgCNVrSveDq&amp;amp;controls=0" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;
&lt;/center&gt;</content><category term="blog"/></entry><entry><title>Book release: DeepSeek in Practice</title><link href="https://duarteocarmo.com/blog/book-release-deepseek-in-practice.html" rel="alternate"/><published>2025-11-17T00:00:00+01:00</published><updated>2025-11-17T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-11-17:/blog/book-release-deepseek-in-practice.html</id><summary type="html">&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://www.packtpub.com/en-us/product/deepseek-in-practice-9781806020850" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/89/cover_image.webp" alt="DeepSeek in Practice book Packt" class="shadow" style="max-width: 40%"&gt;
&lt;/a&gt;
&lt;/center&gt;
&lt;p&gt;&lt;br&gt;
Back in May this year, my longtime friend &lt;a href="https://mlops.systems/about.html"&gt;Alex&lt;/a&gt; reached out and asked me if I wanted to collaborate with him on a book about DeepSeek. I would love to tell you the story of how I thought long and hard before getting back to him. I didn't. I just …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://www.packtpub.com/en-us/product/deepseek-in-practice-9781806020850" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/89/cover_image.webp" alt="DeepSeek in Practice book Packt" class="shadow" style="max-width: 40%"&gt;
&lt;/a&gt;
&lt;/center&gt;
&lt;p&gt;&lt;br&gt;
Back in May this year, my longtime friend &lt;a href="https://mlops.systems/about.html"&gt;Alex&lt;/a&gt; reached out and asked me if I wanted to collaborate with him on a book about DeepSeek. I would love to tell you the story of how I thought long and hard before getting back to him. I didn't. I just said yes.&lt;/p&gt;
&lt;p&gt;Now, 6 months later, I'm excited to announce that &lt;a href="https://www.packtpub.com/en-us/product/deepseek-in-practice-9781806020850"&gt;&lt;em&gt;DeepSeek in Practice&lt;/em&gt;&lt;/a&gt; is out and available for pre-order! The book is published by &lt;a href="https://en.wikipedia.org/wiki/Packt"&gt;Packt&lt;/a&gt; and is a collaboration between &lt;a href="https://pengandy.com/"&gt;Andy Peng&lt;/a&gt;, &lt;a href="https://mlops.systems/about.html"&gt;Alex Strick van Linschoten&lt;/a&gt;, and me.&lt;/p&gt;
&lt;p&gt;As the title suggests, this book focuses on DeepSeek models, which &lt;a href="https://www.nytimes.com/2025/01/27/technology/what-is-deepseek-china-ai.html"&gt;famously took the world by storm earlier this year&lt;/a&gt;. It's organized into three parts: "Understanding and Exploring DeepSeek" examines their role in the wider LLM world. "Using DeepSeek" is entirely dedicated to applying DeepSeek models to real-world problems. Finally, "Distilling and Deploying DeepSeek," covers distillation and deployment.&lt;/p&gt;
&lt;p&gt;Even though the book was a joint effort, my main focus was on Part 2, and how to use DeepSeek models to tackle real-world problems. In particular two chapters:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chapter 5, &lt;em&gt;Building with DeepSeek&lt;/em&gt;,&lt;/strong&gt; walks through creating an alternative to Garmin's daily summary notifications. We go through building a prototype using DeepSeek's API, how to leverage local models, frameworks like &lt;a href="https://github.com/mlc-ai/xgrammar"&gt;XGrammars&lt;/a&gt; and &lt;a href="https://docs.vllm.ai/en/latest/"&gt;vLLM&lt;/a&gt;, all the way to deploying your own model using &lt;a href="https://docs.djl.ai/master/docs/serving/serving/docs/lmi/index.html"&gt;AWS Large Model Inference&lt;/a&gt; (LMI).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chapter 6, &lt;em&gt;Agents with DeepSeek&lt;/em&gt;,&lt;/strong&gt; is all about agents. We start with a short intro to agents, tools, and the inner workings of the &lt;a href="https://modelcontextprotocol.io/docs/getting-started/intro"&gt;MCP protocol&lt;/a&gt;. After that, we build (from scratch) three different agents powered by DeepSeek models: an evaluator-optimizer that summarizes Arxiv papers, an orchestrator-worker that generates research reports, and a tool-calling agent that can search the web and answer complex questions.&lt;/p&gt;
&lt;p&gt;Overall, I'm really excited that this book finally gets to see the light of day. Contributing to a book is no small feat. The process is rough: lots of discussion and lots of drafts thrown in the trash. It was hard work, but also a lot of fun.&lt;/p&gt;
&lt;p&gt;I hope you enjoy it!&lt;/p&gt;
&lt;p&gt;Links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.packtpub.com/en-us/product/deepseek-in-practice-9781806020850"&gt;Packt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://a.co/d/8I139tG"&gt;Amazon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/PacktPublishing/DeepSeek-in-Practice"&gt;GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="blog"/></entry><entry><title>TTS still sucks</title><link href="https://duarteocarmo.com/blog/tts-still-sucks.html" rel="alternate"/><published>2025-11-10T00:00:00+01:00</published><updated>2025-11-10T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-11-10:/blog/tts-still-sucks.html</id><summary type="html">&lt;p&gt;or at least the open versions of it. I have this very stupid rule. A couple of years ago I decided to &lt;a href="https://duarteocarmo.com/blog/you-can-now-listen-to-this-blog"&gt;turn this blog into a podcast&lt;/a&gt;. At the time, I decided to make up a stupid rule: whatever model I use to clone my voice and generate article …&lt;/p&gt;</summary><content type="html">&lt;p&gt;or at least the open versions of it. I have this very stupid rule. A couple of years ago I decided to &lt;a href="https://duarteocarmo.com/blog/you-can-now-listen-to-this-blog"&gt;turn this blog into a podcast&lt;/a&gt;. At the time, I decided to make up a stupid rule: whatever model I use to clone my voice and generate article transcripts needs to be an open model.&lt;/p&gt;
&lt;p&gt;Why? Because - as you might have figured by now - I like to make my life hard. The last version of the podcast generation engine was running on &lt;a href="https://arxiv.org/abs/2410.06885"&gt;F5-TTS&lt;/a&gt;. It was &lt;em&gt;fine&lt;/em&gt;. I still got some funny messages from people showing me the model completely hallucinating or squeaking here and there. But a year later - I was pretty sure there would be something incredibly better out there.&lt;/p&gt;
&lt;p&gt;Now I’m not so sure.&lt;/p&gt;
&lt;p&gt;The first step was to look for the best TTS models out there. Thankfully, Artificial Analysis now publishes a leaderboard with the “best” &lt;a href="https://artificialanalysis.ai/text-to-speech/leaderboard"&gt;text-to-speech models&lt;/a&gt;. After filtering by my stupid rule of open models, we get the below ranking.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/88/tts-rankings.png" alt="tts rankings"&gt;&lt;/p&gt;
&lt;p&gt;At the top of the leaderboard is &lt;a href="https://huggingface.co/hexgrad/Kokoro-82M"&gt;Kokoro&lt;/a&gt;. Kokoro is an amazing model! Especially for a modest 82 Million (!) parameters and a mere 360 MB (!). However, like many models in this leaderboard - I can’t use it - since it doesn’t support voice cloning.&lt;/p&gt;
&lt;p&gt;I started by looking at some of the stuff from &lt;a href="https://fish.audio/"&gt;Fish Audio&lt;/a&gt;. Their &lt;a href="https://github.com/fishaudio/fish-speech"&gt;codebase&lt;/a&gt; seems to now support their new &lt;a href="https://huggingface.co/fishaudio/openaudio-s1-mini"&gt;S1-mini model&lt;/a&gt;. When testing it, most of the &lt;a href="https://github.com/fishaudio/fish-speech?tab=readme-ov-file#speech-control"&gt;emotion markers&lt;/a&gt; did not work - or were only available in their closed version. The breaks and long pauses either. Also, the &lt;a href="https://github.com/search?q=repo%3Afishaudio%2Ffish-speech%20chunk&amp;amp;type=code"&gt;chunking&lt;/a&gt; parameter is completely unused throughout the codebase - so not sure why it’s there. It’s a common business model nowadays: announce a state of the art open model just to attract attention to your real, and the incredible powerful gated model you have to pay for.&lt;/p&gt;
&lt;p&gt;My second-best option on the list was &lt;a href="https://github.com/resemble-ai/chatterbox"&gt;Chatterbox&lt;/a&gt;. This wave of TTS models comes with major limitations. They're all restricted to short character counts - around 1,000–2,000 characters, sometimes even less. Ask them to generate anything longer, and the voice starts hallucinating or speeds up uncontrollably.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://huggingface.co/coqui/XTTS-v2"&gt;XTTS-v2&lt;/a&gt;&lt;/strong&gt;
&lt;audio controls style="width: 75%; display: block;" preload="metadata"&gt;&lt;source src="https://r2.duarteocarmo.com/old/xtts_v2.mp3" type="audio/mpeg"&gt;&lt;/audio&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/SWivid/F5-TTS"&gt;F5-TTS&lt;/a&gt;&lt;/strong&gt;
&lt;audio controls style="width: 75%; display: block" preload="metadata"&gt;&lt;source src="https://r2.duarteocarmo.com/old/f5_tts.mp3" type="audio/mpeg"&gt;&lt;/audio&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/resemble-ai/chatterbox"&gt;Chatterbox&lt;/a&gt;&lt;/strong&gt; (latest version)
&lt;audio controls style="width: 75%; display: block" preload="metadata"&gt;&lt;source src="https://r2.duarteocarmo.com/old/chatterbox.mp3" type="audio/mpeg"&gt;&lt;/audio&gt;&lt;/p&gt;
&lt;p&gt;The transcript generation process is straightforward. First, text gets extracted from my RSS feed and &lt;a href="https://www.notion.so/TTS-still-sucks-29caecef3e2c80bc81ddc9484cab8c59?pvs=21"&gt;pre-processed by an LLM&lt;/a&gt; to make it more "readable". The LLM generates a transcript, a short summary, and a list of links for the show notes. We then chunk the transcript and &lt;a href="https://github.com/duarteocarmo/podcaster/blob/138d64ad083c2e4355e5f92980762b2d9af7c133/src/podcaster/transcription.py#L52"&gt;fire that off to a bunch of parallel Modal containers&lt;/a&gt; where we run the Chatterbox TTS model. Once we get everything back, we stitch the wav files together, and &lt;em&gt;voilà&lt;/em&gt;! The episode is ready. The hosting is an S3 bucket. Really, that’s what you are paying your podcast host for - it’s a lucrative business!&lt;/p&gt;
&lt;iframe allow="autoplay *; encrypted-media *; fullscreen *; clipboard-write" frameborder="0" height="450" style="width:100%;max-width:660px;overflow:hidden;border-radius:10px;" sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation" src="https://embed.podcasts.apple.com/us/podcast/duarte-o-carmos-articles/id1719493997"&gt;&lt;/iframe&gt;

&lt;p&gt;I also made some improvements to the podcast generation side of things. First of all, the podcast is now also available on &lt;a href="https://open.spotify.com/show/0qIVAs1ZDWnpJOQeMo1OjY?si=995d03c12ce749bd"&gt;Spotify&lt;/a&gt;. Additionally, I fixed the show notes to now include nice clickable links in almost every podcast player. Looking at you Apple and your &lt;code&gt;CDATA&lt;/code&gt; &lt;a href="https://help.apple.com/itc/podcasts_connect/#/itcb54353390"&gt;requirements&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Some thoughts on the Chatterbox model. It’s definitely better than F5-TTS. But there are however, some common annoyances with almost every open-source voice cloning model. The first is the limited duration of the generated speech. Anything over 1000 characters starts hallucinating. The second is lack of &lt;em&gt;control&lt;/em&gt;. Some models have &lt;a href="https://github.com/fishaudio/fish-speech?tab=readme-ov-file#speech-control"&gt;emotion tags&lt;/a&gt;, others have &lt;code&gt;&amp;lt;pause&amp;gt;&lt;/code&gt; indicators. But almost every single one of these has been massively unreliable. To the point where I am splitting my text in a sentence per line and shipping that off to the TTS model to make things a bit more reliable.&lt;/p&gt;
&lt;p&gt;So yes, from one side, TTS has come a long way. But when compared to &lt;a href="https://elevenlabs.io/voice-cloning"&gt;other&lt;/a&gt; &lt;a href="https://www.minimax.io/audio"&gt;proprietary&lt;/a&gt; &lt;a href="https://inworld.ai/tts"&gt;systems&lt;/a&gt;, TTS still sucks.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: The rss to podcast pipeline is open source and available if you want to re-use it in &lt;a href="https://github.com/duarteocarmo/podcaster"&gt;this GitHub repo&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Retrospectiva #1</title><link href="https://duarteocarmo.com/blog/retrospectiva-1.html" rel="alternate"/><published>2025-10-31T00:00:00+01:00</published><updated>2025-10-31T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-10-31:/blog/retrospectiva-1.html</id><summary type="html">&lt;p&gt;Welcome to &lt;em&gt;Retrospectiva.&lt;/em&gt; Retrospectiva is a monthly update about what I’ve been up to. In the age of LLMs, I’ve heard many argue that it’s hard (and useless) to write anything at all anymore. When anyone can prompt a model and get some text from the magic …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Welcome to &lt;em&gt;Retrospectiva.&lt;/em&gt; Retrospectiva is a monthly update about what I’ve been up to. In the age of LLMs, I’ve heard many argue that it’s hard (and useless) to write anything at all anymore. When anyone can prompt a model and get some text from the magic box, what’s the point?&lt;/p&gt;
&lt;p&gt;I couldn’t disagree more. Creativity, personality, and &lt;em&gt;good taste&lt;/em&gt; have never been so important. If anything, I’m planning to write more.&lt;/p&gt;
&lt;p&gt;Winter has arrived in Denmark, and the sun is setting at around 4PM. It’s what we call the &lt;a href="https://www.oed.com/dictionary/hygge_n"&gt;hygge&lt;/a&gt; period. It’s dark, cold, and rainy most of the time - and so we make the most out of staying inside.&lt;/p&gt;
&lt;h2 id="using"&gt;Using&lt;/h2&gt;
&lt;p&gt;The endless chase for the perfect browser continues. I’ve been a long time Firefox user, until things started breaking, and I moved to Chrome. I hate Chrome. Don’t get me wrong - Chrome works 99% of the time - but I like taking Google products with a grain of salt.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://helium.computer/"&gt;Helium&lt;/a&gt; has been a breath of fresh air. The guts are the same as Chrome, but it’s de-googlified and completely &lt;a href="https://github.com/imputnet/helium"&gt;open-source&lt;/a&gt;. One Chrome feature I rely on is the built-in webpage translation. Chrome’s Google Translate’s integration does this better than anything else. But &lt;a href="https://github.com/FilipePS/Traduzir-paginas-web"&gt;this little alternative&lt;/a&gt; has worked great for me.&lt;/p&gt;
&lt;p&gt;It’s a fruitful period for tech! In the AI-tool space there’s &lt;a href="https://www.claude.com/product/claude-code"&gt;Claude Code&lt;/a&gt;, &lt;a href="https://developers.openai.com/codex/cli/"&gt;Codex&lt;/a&gt;, &lt;a href="https://cursor.com/blog/2-0"&gt;Cursor&lt;/a&gt;, &lt;a href="https://cline.bot/"&gt;Cline&lt;/a&gt;, &lt;a href="https://github.com/features/copilot/cli"&gt;Copilot&lt;/a&gt;, and probably 54 others. We are the ones that get to benefit from every company fighting to get us to use their tools.&lt;/p&gt;
&lt;p&gt;When I’m asked about what I’m using, my answer has consistently been: “It changes every week”. Recently I’ve gone back to what I love: &lt;a href="https://neovim.io/"&gt;Neovim&lt;/a&gt;, &lt;a href="https://github.com/tmux/tmux/wiki"&gt;tmux&lt;/a&gt;, and a couple of CLI based agents. To pull everything together &lt;a href="https://github.com/folke/sidekick.nvim"&gt;Sidekick&lt;/a&gt; has been great.&lt;/p&gt;
&lt;h2 id="reading"&gt;Reading&lt;/h2&gt;
&lt;p&gt;For most of 2025, I’ve switched between &lt;a href="https://duarteocarmo.com/blog/goodbye-kindle-i-dont-think-ill-miss-you"&gt;digital&lt;/a&gt; and physical books. But living in a small Danish apartment means I don’t have a bedside light when reading at night. Vitto got me this little &lt;a href="https://www.igritin.com/products/gritin-9-led-rechargeable-book-light-for-reading-in-bed"&gt;20 USD light&lt;/a&gt; and I’ve been back on the physical book train.&lt;/p&gt;
&lt;p&gt;Two highlights. The first is &lt;a href="https://www.lulu.com/shop/sean-goedecke/software-engineering-after-the-vibe-shift/paperback/product-jew4ver.html?q=Sean+Goedecke&amp;amp;page=1&amp;amp;pageSize=4"&gt;Software Engineering after the Vibe Shift&lt;/a&gt; by Sean Goedecke. It’s a short but insightful book. Even though I don’t consider myself a Software Engineer (I’m more on the Machine Learning side of things) a considerable amount of ideas hit home:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;«&lt;em&gt;The best way to anticipate problems is to deploy early. In general, a helpful question to ask is &lt;strong&gt;can I ship this right now?&lt;/strong&gt; Not this week, not today: right this second. If not, what would have to change for me to be able to ship something?»&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I’m finishing up &lt;a href="https://www.manning.com/books/machine-learning-system-design"&gt;Machine Learning System Design&lt;/a&gt; by Valerii Babushkin and Arseny Kravchenko. It’s a great read so far, with lots of interesting fireside stories about building machine learning systems at scale. It’s funny how a simple binary classification can get complicated when you are building at scale.&lt;/p&gt;
&lt;p&gt;I need to get back to reading some fiction.&lt;/p&gt;
&lt;h2 id="listening"&gt;Listening&lt;/h2&gt;
&lt;p&gt;Vitto asked me to move off Spotify with her. This is not a political blog - she has her reasons. So I took the plunge. After all, it’s fairly easy to move &lt;a href="https://support.apple.com/en-us/118249"&gt;my playlists around&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Overall: mixed bag. I could get around all the Google Home limitations by running &lt;a href="https://github.com/philippe44/AirConnect"&gt;Airconnect&lt;/a&gt; on my Raspberry Pi, and I understand the focus of Apple Music vs. Spotify is much about quality vs. quantity. But I’m not sure I’m sold. The Apple Music app on the Mac is buggy, and the playlist diversity is just not there. I want “Chill Italian Rap” - Spotify has at least 45 playlist results - how come Apple has none? And don’t get me started &lt;a href="https://discussions.apple.com/thread/255774369?sortBy=rank"&gt;on this&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For podcasts, I have two recommendations. &lt;a href="https://www.dwarkesh.com/p/andrej-karpathy"&gt;Karpathy's interview with Dwarkesh&lt;/a&gt; is a must-listen. I found myself smiling regularly throughout this one. Another great listen was &lt;a href="https://newsletter.pragmaticengineer.com/p/python-go-rust-typescript-and-ai"&gt;Armin Ronacher’s interview&lt;/a&gt; on the Pragmatic Engineer.&lt;/p&gt;
&lt;h2 id="watching"&gt;Watching&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Q0TpWitfxPk"&gt;This video from Hank Green about the state of the AI industry&lt;/a&gt; is a very interesting watch (and commentary on &lt;a href="https://archive.vn/V0dWR"&gt;this article&lt;/a&gt;). Sure, he’s funny and entertaining, &lt;a href="https://www.publico.pt/2025/10/08/opiniao/opiniao/ia-bolha-utilidade-2150129"&gt;but I also agree on a lot&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But not everything is about ML and AI - and since it's winter - we also took some time to get some new blankets and fire up the old projector. HBO's &lt;a href="https://www.rottentomatoes.com/tv/a_body_in_the_snow_the_trial_of_karen_read/s01"&gt;Trial of Karen Read&lt;/a&gt; wasn't the most &lt;em&gt;intellectual&lt;/em&gt; watch but it was certainly entertaining. Recommend that one for the “binge in a day” category.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Faísca: The modern LLM stack in a single script</title><link href="https://duarteocarmo.com/blog/faisca-the-modern-llm-stack-in-a-single-script.html" rel="alternate"/><published>2025-10-15T00:00:00+02:00</published><updated>2025-10-15T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-10-15:/blog/faisca-the-modern-llm-stack-in-a-single-script.html</id><summary type="html">&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/86/cover.webp" alt="Flower Clouds - Odilon Redon" style="max-width:100%;border-radius: 2px;"&gt;&lt;/p&gt;
&lt;h2 id="why-do-this"&gt;Why do this?&lt;/h2&gt;
&lt;p&gt;ML and AI are moving at an incredible pace. The amount of research coming out vastly surpasses anyone's ability to &lt;em&gt;interiorize&lt;/em&gt; it. By interiorize I mean study, experiment, or even just test it out. However, the importance of learning has never been greater. Even if we don't …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/86/cover.webp" alt="Flower Clouds - Odilon Redon" style="max-width:100%;border-radius: 2px;"&gt;&lt;/p&gt;
&lt;h2 id="why-do-this"&gt;Why do this?&lt;/h2&gt;
&lt;p&gt;ML and AI are moving at an incredible pace. The amount of research coming out vastly surpasses anyone's ability to &lt;em&gt;interiorize&lt;/em&gt; it. By interiorize I mean study, experiment, or even just test it out. However, the importance of learning has never been greater. Even if we don't really know what the future looks like, time studying, reading, and building is &lt;em&gt;never&lt;/em&gt; wasted.&lt;/p&gt;
&lt;p&gt;I've always preferred pragmatic resources over theoretical ones. I learn best by doing. Resources like Andrej's &lt;a href="https://github.com/karpathy/minGPT"&gt;minGPT&lt;/a&gt; and Raschka's &lt;a href="https://github.com/rasbt/LLMs-from-scratch"&gt;LLM from scratch series&lt;/a&gt; are the ones I learn best from. So, I decided to build my own minimal implementation of an LLM that captures most of the concepts around "modern" training. It's called: &lt;a href="https://github.com/duarteocarmo/faisca"&gt;Faísca&lt;/a&gt;&lt;sup id="sf-faisca-the-modern-llm-stack-in-a-single-script-1-back"&gt;&lt;a href="#sf-faisca-the-modern-llm-stack-in-a-single-script-1" class="simple-footnote" title="Means spark in Portuguese"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Faísca is an implementation of the "modern" LLM stack in a single script of around ~1000 lines of code. All you need to start training is &lt;a href="https://docs.astral.sh/uv/getting-started/installation/"&gt;uv&lt;/a&gt;:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;uv&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;https://raw.githubusercontent.com/duarteocarmo/faisca/refs/heads/master/faisca_torch.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I built it as an educational resource I can hack on over time. Right now, it trains a model that generates newspaper headlines in Portuguese. Simple? Absolutely. It's by no means comparable to ChatGPT. Faísca has &lt;em&gt;only&lt;/em&gt; 13 million parameters. (For context, GLM 4.6 -  the best performing open source model in LMArena - has ~355 billion&lt;sup id="sf-faisca-the-modern-llm-stack-in-a-single-script-2-back"&gt;&lt;a href="#sf-faisca-the-modern-llm-stack-in-a-single-script-2" class="simple-footnote" title="With only 32 active but still"&gt;2&lt;/a&gt;&lt;/sup&gt;: Faísca is &lt;strong&gt;~27.000 times smaller&lt;/strong&gt;)&lt;/p&gt;
&lt;p&gt;If you just want the code, &lt;a href="https://github.com/duarteocarmo/faisca/blob/master/faisca_torch.py"&gt;it's here&lt;/a&gt;. For the rest, let me walk you through how it works.&lt;/p&gt;
&lt;h2 id="a-small-dataset-of-news-headlines"&gt;A small dataset of news headlines&lt;/h2&gt;
&lt;p&gt;Faisca can work with any dataset. For this example, I started with something close to my heart: &lt;a href="/about"&gt;Portuguese&lt;/a&gt;. I built a &lt;a href="https://huggingface.co/datasets/duarteocarmo/ccnews-titles-2016"&gt;dataset&lt;/a&gt; that filters &lt;a href="https://commoncrawl.org/news-crawl"&gt;Common Crawl News&lt;/a&gt; for only 2016. The resulting &lt;a href="https://huggingface.co/datasets/duarteocarmo/ccnews-titles-2016"&gt;dataset&lt;/a&gt; has 1.8 million headlines from publications around the world.&lt;/p&gt;
&lt;p&gt;I also kept the language identified from Common Crawl as well as the original url.&lt;/p&gt;
&lt;p&gt;&lt;iframe src="https://huggingface.co/datasets/duarteocarmo/ccnews-titles-2016/embed/viewer/default/train" frameborder="0" width="100%" height="560px"&gt;&lt;/iframe&gt;
&lt;/p&gt;
&lt;h2 id="gpt2-in-pytorch"&gt;GPT2 in PyTorch&lt;/h2&gt;
&lt;p&gt;FaiscaGPT is inspired by both minGPT and LLM-from-scratch. It's a mix that implements the &lt;a href="https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf"&gt;GPT2&lt;/a&gt; architecture using only &lt;a href="https://pytorch.org/"&gt;PyTorch&lt;/a&gt; (the only dependency). At the core, you'll notice the Transformer Block with MultiHeadedAttention. In the vanilla state, the transformer block has 4 heads, an embedding dimension of 128, and 4 layers. This means the whole thing is around 13 million parameters (e.g., should run fine in modern MacBooks!)&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;TransformerBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;embedding_dimension&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;num_heads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;qkv_bias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;dropout_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attention&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MultiheadAttention&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;embed_dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embedding_dimension&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;num_heads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;num_heads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;dropout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dropout_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;bias&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;qkv_bias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;batch_first&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;feed_forward&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FeedForward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;embedding_dimension&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embedding_dimension&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;norm1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LayerNorm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding_dimension&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;norm2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LayerNorm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding_dimension&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop_shortcut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dropout_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FaiscaGPT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token_embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vocab_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding_dimension&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;positional_embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Embedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding_dimension&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dropout_embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dropout_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transformer_blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="n"&gt;TransformerBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;embedding_dimension&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding_dimension&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;num_heads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_heads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;qkv_bias&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qkv_bias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;dropout_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dropout_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_layers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;final_layer_norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LayerNorm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding_dimension&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out_head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding_dimension&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vocab_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bias&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;n_params_all&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;n_params_all_million&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n_params_all&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1e6&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Total number of params: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n_params_all_million&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;M"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If this is too big/small for you, you can tweak the settings in the &lt;a href="https://github.com/duarteocarmo/faisca/blob/master/faisca_torch.py#L979"&gt;config&lt;/a&gt;. I wanted something powerful, &lt;em&gt;mostly&lt;/em&gt; bug-free (I hope), and that isn't overwhelming for someone to tweak it.&lt;/p&gt;
&lt;p&gt;That's nice and all, but what about training?&lt;/p&gt;
&lt;h2 id="pre-training-headlines-in-portuguese"&gt;Pre-training headlines in Portuguese&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/86/faisca_2025-10-04_22-02-58.png" alt="faisca"&gt;&lt;/p&gt;
&lt;p&gt;The first phase is pre-training. We filter the dataset for headlines in the Portuguese language. We then train on around 30K headlines over 10 epochs. At the start the model outputs gibberish, eventually settling into something more coherent. Even if not grammatically correct, it certainly &lt;em&gt;looks&lt;/em&gt; plausible.&lt;/p&gt;
&lt;p&gt;If we wanted to make it speak perfect Portuguese, we would train it on more data for longer (following the &lt;a href="https://arxiv.org/abs/2203.15556"&gt;Chinchilla law&lt;/a&gt;, the optimal training data for such a model would be around 250M tokens). But that's not the goal of this exercise. However, I did notice a strong bias towards Brazilian Portuguese - expected, since it's the most common variant of Portuguese.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;...
Epoch: 8 - Step: 1540 - Train Loss: 3.7417 - Validation Loss: 4.0037 - Batch: 37 / 188 - Tokens seen: 25247744
Epoch: 8 - Step: 1560 - Train Loss: 3.7077 - Validation Loss: 4.0016 - Batch: 57 / 188 - Tokens seen: 25575424
Epoch: 8 - Step: 1580 - Train Loss: 3.7250 - Validation Loss: 3.9938 - Batch: 77 / 188 - Tokens seen: 25903104
Epoch: 8 - Step: 1600 - Train Loss: 3.7074 - Validation Loss: 3.9858 - Batch: 97 / 188 - Tokens seen: 26230784
Epoch: 8 - Step: 1620 - Train Loss: 3.7159 - Validation Loss: 3.9816 - Batch: 117 / 188 - Tokens seen: 26558464
Epoch: 8 - Step: 1640 - Train Loss: 3.7063 - Validation Loss: 3.9745 - Batch: 137 / 188 - Tokens seen: 26886144
Epoch: 8 - Step: 1660 - Train Loss: 3.6965 - Validation Loss: 3.9735 - Batch: 157 / 188 - Tokens seen: 27213824
Epoch: 8 - Step: 1680 - Train Loss: 3.6909 - Validation Loss: 3.9691 - Batch: 177 / 188 - Tokens seen: 27541504
**** GENERATION 1 OF 1 ****
&amp;gt; 'Presidente com o que os quase 3 milhões de seução'
&amp;gt; 'Governo para ao de fazer em São de casação deixa'
&amp;gt; 'Cânia de caminhão do ensino, diz que veítica de lider'
*************************
Epoch: 9 - Step: 1700 - Train Loss: 3.6883 - Validation Loss: 3.9643 - Batch: 9 / 188 - Tokens seen: 27869184
Epoch: 9 - Step: 1720 - Train Loss: 3.6987 - Validation Loss: 3.9607 - Batch: 29 / 188 - Tokens seen: 28196864
Epoch: 9 - Step: 1740 - Train Loss: 3.6507 - Validation Loss: 3.9561 - Batch: 49 / 188 - Tokens seen: 28524544
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But how could we direct the model towards more of a Portuguese from Portugal? Enter our second training stage.&lt;/p&gt;
&lt;h2 id="supervised-fine-tuning-sft-on-portuguese-from-portugal"&gt;Supervised fine-tuning (SFT) on Portuguese from Portugal&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/86/faisca_2025-10-04_22-02-58_sft.png" alt="faisca sft"&gt;&lt;/p&gt;
&lt;p&gt;Supervised fine-tuning is &lt;em&gt;just&lt;/em&gt; more training. In the case of ChatGPT and variants this is where the models get on multi-turn conversations. In our case though, we want something slightly different.&lt;/p&gt;
&lt;p&gt;We re-filter the training data to include only headlines from websites that finish with &lt;code&gt;.pt&lt;/code&gt;, to make sure they're from Portugal. And we then train for about 5 epochs of 20K titles. And guess what? The effect is noticeable! After just a couple of epochs, the model's text feels much more like Portuguese from Portugal than Brazil (words like "Cristiano", "Portugal", the accents, and the tone).&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;...
Epoch: 3 - Step: 85 - Train Loss: 3.6417 - Validation Loss: 4.1149 - Batch: 14 / 24 - Tokens seen: 1409024
Epoch: 3 - Step: 90 - Train Loss: 3.6514 - Validation Loss: 4.1140 - Batch: 19 / 24 - Tokens seen: 1490944
Epoch: 3 - Step: 95 - Train Loss: 3.6601 - Validation Loss: 4.1088 - Batch: 24 / 24 - Tokens seen: 1572864
**** GENERATION 1 OF 1 ****
&amp;gt; 'Presidente-se ao de Festa'
&amp;gt; 'Actualidade: «Novação da Saúbal e um mortos'
&amp;gt; 'Actualidade: "Aumento deixação ao de atentado em Portugal'
&amp;gt; 'Actualidade: «Papa Francisco é'
*************************
Epoch: 4 - Step: 100 - Train Loss: 3.6414 - Validation Loss: 4.1060 - Batch: 5 / 24 - Tokens seen: 1654784
Epoch: 4 - Step: 105 - Train Loss: 3.6308 - Validation Loss: 4.1045 - Batch: 10 / 24 - Tokens seen: 1736704
Epoch: 4 - Step: 110 - Train Loss: 3.6047 - Validation Loss: 4.0994 - Batch: 15 / 24 - Tokens seen: 1818624
Epoch: 4 - Step: 115 - Train Loss: 3.6141 - Validation Loss: 4.0964 - Batch: 20 / 24 - Tokens seen: 1900544
**** GENERATION 1 OF 1 ****
&amp;gt; 'Presidente: «Quero e ao de mais devembia'
&amp;gt; 'Fernando de ano como de saíria'
&amp;gt; 'Actualidade: Pelo com ao de mais de Berlim foições'
&amp;gt; 'Cristiano Oriental: «Estamos'
*************************
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What if we wanted to direct our model even more? What if all we wanted was football related headlines? Enter our final phase.&lt;/p&gt;
&lt;h2 id="reinforcement-learning-grpo-for-sports-news"&gt;Reinforcement Learning (GRPO) for sports news&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/86/faisca_2025-10-07_20-21-28_rl.png" alt="faisca rl"&gt;&lt;/p&gt;
&lt;p&gt;The last phase is all about &lt;a href="https://en.wikipedia.org/wiki/Reinforcement_learning"&gt;reinforcement learning&lt;/a&gt; (RL). The &lt;a href="https://openai.com/index/learning-from-human-preferences/"&gt;cherry on top of the cake&lt;/a&gt; that made ChatGPT so popular. A couple of years ago we needed to have some sort of &lt;a href="https://huggingface.co/datasets/openbmb/UltraFeedback"&gt;preference data&lt;/a&gt; to do reinforcement learning. In the last couple of years - as LLMs got more popular - other RL techniques came to fruition. Techniques like Proximal Policy Optimization (PPO), and Direct Preference Optimization (DPO) started popping up (&lt;a href="https://arxiv.org/abs/2404.10719"&gt;more reading here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Recently, &lt;a href="https://www.nature.com/articles/s41586-025-09422-z"&gt;DeepSeek took the world by storm&lt;/a&gt; and came up with Group Relative Policy Optimization (&lt;a href="https://arxiv.org/abs/2402.03300"&gt;GRPO&lt;/a&gt;). GRPO speeds up things compared to other RL techniques by removing the need for a critic model. Instead, we sample a group of candidate responses from our LLM, score them using a "reward function", and use those scores to update the model towards the "good responses". In short, we need much less memory to optimize.&lt;/p&gt;
&lt;p&gt;Another beautiful thing about this technique is that we only need a single function to optimize our model:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;calculate_reward_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;target_words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"futebol"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"benfica"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"porto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"sporting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"bola"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"liga"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;has_word&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;intersection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_words&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;has_word&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's it. That's enough to kick start the training process for our tiny model. In the graph above you see the rewards increase for every training step, as well as the KL divergence (that shows us how much the probability distribution shifts from the original).&lt;/p&gt;
&lt;p&gt;In practice, our model is now much more likely to generate headlines related to football - for which it was rewarded. It now mentions words like "Real Madrid" much more often!&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;...
3: kl= 0.2592, grad_norm= 0.7413
Processing experience 1 of 2
3: kl= 0.1728, grad_norm= 0.9851
Step 17 generated 24 completions
=== Sample completions ===
&amp;gt; Mais fé aos de lado pesso dização com o Governo que mais velho
&amp;gt; Mais o FC Porto de um luta contra o mundo aumento com o Sporting
&amp;gt; Mém o que faz apoi vivemio e agora do BES: «Quase um ligação do mundo de trabalho
&amp;gt; Sporting a todos os jogadores ao Real Madrid
=========================
Returns: 16.00/24
Returns of step 17: 16.0000
Processing experience 0 of 2
0: kl= 0.2157, grad_norm= 0.8532
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="final-thoughts-acknowledgements"&gt;Final thoughts &amp;amp; Acknowledgements&lt;/h2&gt;
&lt;p&gt;This was a pretty fun project to hack on, and took a longer than expected. I have some ideas for expanding it, and perhaps make a more &lt;em&gt;Apple-Native&lt;/em&gt; version by using &lt;a href="https://github.com/ml-explore/mlx"&gt;MLX&lt;/a&gt;. Potentially, we could also create a &lt;a href="https://huggingface.co/blog/moe"&gt;Mixture of Experts&lt;/a&gt; variant.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/duarteocarmo/faisca"&gt;The repo is 100% open source&lt;/a&gt; - feel free to fork it, hack on it, and adapt it to your needs! If you find bugs, don't hesitate to submit a PR.&lt;/p&gt;
&lt;p&gt;Finally, I want to acknowledge the great projects that inspired this work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/karpathy/minGPT"&gt;minGPT from Andrej Karpathy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rasbt/LLMs-from-scratch"&gt;LLMs from Scratch - Sebastian Raschka&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mingyin0312/RLFromScratch"&gt;RLFromScratch - Ming Yin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ttumiel/minRLHF"&gt;minRLHF - Tom Tumiel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-faisca-the-modern-llm-stack-in-a-single-script-1"&gt;Means spark in Portuguese &lt;a href="#sf-faisca-the-modern-llm-stack-in-a-single-script-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-faisca-the-modern-llm-stack-in-a-single-script-2"&gt;With only 32 active but still &lt;a href="#sf-faisca-the-modern-llm-stack-in-a-single-script-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>AI: the utility bubble - for Publico.pt</title><link href="https://www-publico-pt.translate.goog/2025/10/08/opiniao/opiniao/ia-bolha-utilidade-2150129?_x_tr_sl=auto&amp;_x_tr_tl=en&amp;_x_tr_hl=en-US&amp;_x_tr_pto=wapp" rel="alternate"/><published>2025-10-09T00:00:00+02:00</published><updated>2025-10-09T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:www-publico-pt.translate.goog,2025-10-09:/2025/10/08/opiniao/opiniao/ia-bolha-utilidade-2150129</id><content type="html"/><category term="blog"/></entry><entry><title>Drowning in News</title><link href="https://duarteocarmo.com/blog/drowning-in-news.html" rel="alternate"/><published>2025-10-05T00:00:00+02:00</published><updated>2025-10-05T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-10-05:/blog/drowning-in-news.html</id><summary type="html">&lt;p&gt;The world moves fast, faster every day. For those who work with technology - and even those who don't - it's hard to keep up with the news. But I've always enjoyed staying up to date with what is happening, and my main tool to get it done hasn't changed in years …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The world moves fast, faster every day. For those who work with technology - and even those who don't - it's hard to keep up with the news. But I've always enjoyed staying up to date with what is happening, and my main tool to get it done hasn't changed in years: it's &lt;a href="https://en.wikipedia.org/wiki/RSS"&gt;RSS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My morning routine has been the same for the past 10 years. Every morning I open up &lt;a href="https://reederapp.com/classic/"&gt;Reeder&lt;/a&gt; on my Mac - or on my phone if I'm on the go - and ctrl+click the articles that interest me the most. The setup is not groundbreaking: a &lt;a href="https://feedly.com/i"&gt;Feedly&lt;/a&gt; account for my feeds, and Reeder as the app to consume them. It's not complicated. But something was bothering me.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/84/reeder.png" alt="reeder classic"&gt;&lt;/p&gt;
&lt;p&gt;RSS lets me stay in control of what's happening. With RSS, I'm not a victim of someone else's algorithm. I decide what gets in front of me and how. But I didn't have that feeling anymore. Messy categories, more and more feeds, non-working feeds, just a firehose of articles everyday. Scanning 100+ titles every morning is not a great way to start the day.&lt;/p&gt;
&lt;p&gt;Like most people nowadays, I bluntly decided to 'throw AI'&amp;reg; at the problem. I exported all my feeds from Feedly into &lt;code&gt;opml&lt;/code&gt; format and created a &lt;a href="https://gist.github.com/duarteocarmo/4869cae3f8c5bd5c95a556cc3a70ece3"&gt;quick script&lt;/a&gt; that uses LLMs to re-categorize all my feeds and reorganize them. That worked for a couple of weeks, but this was not a categorization problem. This was a &lt;em&gt;get back into control&lt;/em&gt; problem.&lt;/p&gt;
&lt;p&gt;I took the plunge and decided to self-host my RSS server. A couple of clicks on &lt;a href="/blog/how-i-self-host-in-2024.html"&gt;my Coolify instance&lt;/a&gt; and &lt;a href="https://freshrss.org/index.html"&gt;FreshRSS&lt;/a&gt; was up and running at &lt;a href="https://news.duarteocarmo.com"&gt;news.duarteocarmo.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My first impressions are great. First, it automatically detects dead or non-working RSS feeds. That led to a lot of 'Oh! I loved following this website' moments. So I spent half an hour cleaning those up. Also, managing the categories of different feeds is just much easier. There's a nice drag and drop view to put everything into place. But still, RSS is an uphill battle.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/84/freshrss.png" alt="freshrss"&gt;&lt;/p&gt;
&lt;p&gt;The world doesn't like RSS, they want you to go to the website and click those links. They'll ask for your email, they'll put up a paywall. Anything to get you to go to the website directly. But there's hope: here are some of my favorite tools to get everything back into your neatly organized RSS feed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What about newsletters?&lt;/strong&gt; &lt;a href="https://kill-the-newsletter.com"&gt;Kill the Newsletter&lt;/a&gt; is the solution. A great app from &lt;a href="https://leafac.com/"&gt;Leandro&lt;/a&gt; that turns any newsletter into an RSS feed you can subscribe to. Better? It's free. I would pay for it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What about Substack?&lt;/strong&gt; I know there's a lot of interesting content on &lt;a href="https://substack.com/"&gt;Substack&lt;/a&gt;, and many people I follow are pretty active there. Here's the good news: every Substack publication &lt;a href="http://support.substack.com/hc/en-us/articles/360038239391-Is-there-an-RSS-feed-for-my-publication"&gt;also offers an RSS feed&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What about Reddit?&lt;/strong&gt; That's a good question. For years, Reddit has been gradually phasing out the ability to follow subreddits via RSS. Throttling and failures have become the norm. &lt;a href="https://feedly.com/new-features/posts/follow-reddit-in-feedly"&gt;Feedly offers a solution for this&lt;/a&gt;, but it hasn't worked reliably for me. I want to follow &lt;em&gt;just the right amount&lt;/em&gt; of Reddit content. Fortunately, there's an excellent open-source project called &lt;a href="https://github.com/johnwarne/reddit-top-rss"&gt;Reddit Top RSS&lt;/a&gt;. I self-hosted it on Coolify (again, just 2 clicks) and now I have a &lt;em&gt;limited&lt;/em&gt; and &lt;em&gt;controlled&lt;/em&gt; feed for every subreddit I'm interested in.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I haven't really scratched the surface of what &lt;a href="https://freshrss.org/index.html"&gt;FreshRSS&lt;/a&gt; can do. Statistics, filtering feeds, extensions, labels, I haven't even begun exploring it all. And honestly? That's fine. My Reeder workflow remains unchanged, my morning routine intact. But underneath, something fundamental has shifted. Just like with my &lt;a href="/blog/hacking-on-my-finances-part-2-beancount-on-beanstalk.html"&gt;finances&lt;/a&gt;, self-hosting gives me something no third-party service can: control.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Common misconceptions about AI</title><link href="https://duarteocarmo.com/blog/common-misconceptions-about-ai.html" rel="alternate"/><published>2025-09-07T00:00:00+02:00</published><updated>2025-09-07T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-09-07:/blog/common-misconceptions-about-ai.html</id><summary type="html">&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/83/gubbio_landscape.webp" alt="gubbio landscape" class="shadow"&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;It's that time of year again. As usual, we took a couple of weeks off and came south: a bit of Portugal, a bit of Italy, a lot of friends and family. My family has a long-running joke that I hate people and love my computer. That's not (entirely) true …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/83/gubbio_landscape.webp" alt="gubbio landscape" class="shadow"&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;It's that time of year again. As usual, we took a couple of weeks off and came south: a bit of Portugal, a bit of Italy, a lot of friends and family. My family has a long-running joke that I hate people and love my computer. That's not (entirely) true. I talked to lots of people this summer. But something felt off.&lt;/p&gt;
&lt;p&gt;ChatGPT this, AI that, Copilot here, Gemini there. Nobody had a clue what they were talking about. I’m not an AI guru, but I have worked in the field for some years. People are not stupid; people are misinformed.&lt;/p&gt;
&lt;p&gt;If you know what a Mixture-of-experts or KV cache is - some of these misconceptions might be surprising. If you don’t, then it’s time to clear some things up.&lt;/p&gt;
&lt;h2 id="large-language-models-are-connected-to-the-internet"&gt;Large Language Models are connected to the internet&lt;/h2&gt;
&lt;p&gt;Large language models (LLMs) are not connected to the internet. A model is a zip file, a file you can even run in your own laptop&lt;sup id="sf-common-misconceptions-about-ai-1-back"&gt;&lt;a href="#sf-common-misconceptions-about-ai-1" class="simple-footnote" title="You should try LM Studio"&gt;1&lt;/a&gt;&lt;/sup&gt;. A file that compresses the whole internet it was trained on. But that training had a beginning and an end. The model knows nothing about what happened after it was trained. Ask a model - in its purest form - the news from today and it will refuse or make something up.&lt;/p&gt;
&lt;p&gt;"But Duarte, when I ask ChatGPT today's news it knows". What ChatGPT - and many other apps - are doing is stuffing search results into the model context. ChatGPT used to have a Search button, remember? (I know my sister Cata was pissed I didn't tell her sooner). ChatGPT still has a "Search Web" button, it just now automatically detects if your question needs a web search and uses it. So you don’t have to think about it.&lt;/p&gt;
&lt;h2 id="large-language-models-should-cite-their-sources"&gt;Large Language Models should cite their sources&lt;/h2&gt;
&lt;p&gt;Let’s say you are training your own LLM. You start by collecting the entire internet into a single Word document. You run a probabilistic model over it. The model starts learning. For example, whenever the model sees “The president of the USA is,” it always sees “Barack Obama” next to it. This is a large document; it has seen that pattern many times—in websites, Wikipedia, blogs, books, etc. And so it learns it.&lt;/p&gt;
&lt;p&gt;Once training stops, you prompt the model: “The president of the USA is,” and it responds: “Barack Obama,” simply because it is &lt;em&gt;likely&lt;/em&gt;. It sounds wrong today, and that’s exactly the point: the model only knows what it saw during training. How can we cite this process? It wasn’t a single website that made the LLM respond like that. It was the effect of the entire corpus. But what exactly has it seen?—you might ask—and how much? Well, that’s where things get &lt;a href="https://www.nytimes.com/2025/09/05/technology/anthropic-settlement-copyright-ai.html"&gt;tricky&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="an-ice-cream-flavor-generated-by-artificial-intelligence"&gt;An Ice-cream flavor generated by artificial intelligence&lt;/h2&gt;
&lt;p&gt;There are a lot of things that can be generated by AI. Some of them good, some of them less bad, a lot of them useless. Just because you &lt;em&gt;can&lt;/em&gt; do something with AI does not mean you should. Someone prompted ChatGPT for weird flavors of ice-cream, posted them online and got thousands of likes. We have a name for that. It’s called &lt;a href="https://en.wikipedia.org/wiki/AI_slop"&gt;slop&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;$$
\text{AI Generated} \neq \text{Good}
$$&lt;/p&gt;
&lt;p&gt;Half our feeds are low quality AI content, the other is filled with influencers that &lt;em&gt;know&lt;/em&gt; that adding the two magic letters will triple the interactions. In summary: be skeptical of anything with “AI” slapped on it.&lt;/p&gt;
&lt;h2 id="it-automatically-learns-from-what-you-tell-it"&gt;It automatically learns from what you tell it&lt;/h2&gt;
&lt;p&gt;My entire family thinks AI is some sort of all powerful monster that learns continuously from whatever they tell it. Another myth. A model is an artifact that is stuck in time. In order for it to learn something new, you need to retrain it. Retraining is a long and expensive process. If you tell ChatGPT that your mom's name is Elsa, and create another conversation, the &lt;em&gt;model&lt;/em&gt; itself does &lt;em&gt;not&lt;/em&gt; know that. &lt;sup id="sf-common-misconceptions-about-ai-2-back"&gt;&lt;a href="#sf-common-misconceptions-about-ai-2" class="simple-footnote" title="Even if they use your data for training - but this loop takes a while - it's not instant"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;I know if feels like it's continuously learning. AI providers are use small "tricks" to make that happen.They write what you said to it in a file, and then inject that file into the context when you start a conversation. Effectively giving you the idea it's learning even though it’s not. The model needs to be retrained to get new knowledge.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;
&lt;img src="https://duarteocarmo.com/images/83/roma.webp" alt="Roma pizza" class="shadow"&gt;
&lt;br&gt;&lt;/p&gt;
&lt;h2 id="are-we-in-a-bubble"&gt;Are we in a bubble?&lt;/h2&gt;
&lt;p&gt;Of course we are, even &lt;a href="https://www.cnbc.com/2025/08/18/openai-sam-altman-warns-ai-market-is-in-a-bubble.html"&gt;Sam has said it&lt;/a&gt;. Many companies are pivoting overnight to AI, and getting crazy investments as a byproduct, much like the dot-com days. Researchers are getting paid &lt;a href="https://www.nytimes.com/2025/07/31/technology/ai-researchers-nba-stars.html"&gt;millions of dollars&lt;/a&gt;. Influencers are getting crazy engagement just by mentioning AI. This is peak stupid.&lt;/p&gt;
&lt;p&gt;But AI is also insanely useful. That has real effects. Just look at your everyday now: Are you using Google as much? Are you taking advantage of models in any way? I am pretty sure you are. These models are useful. These models are powerful. And the open source ecosystem of models is genuinely growing and exciting. The whole world in your pocket? The whole internet’s knowledge without WiFi!&lt;/p&gt;
&lt;p&gt;As usual, we are somewhere in the middle of the useful and the overhyped.&lt;/p&gt;
&lt;h2 id="humanity-is-doomed-and-technology-sucks"&gt;Humanity is doomed and technology sucks&lt;/h2&gt;
&lt;p&gt;Why are we investing money in going to Mars? Why are we inventing these new things that destroy creativity? Why are we submitting our children to such a future? We should go back to pen and paper, we should stop this right now, we should invest only in our planet and nothing else. I understand, change is scary, we are scared of the unknown. Especially when we are older and used to things in their &lt;em&gt;normal&lt;/em&gt; state - whatever that is.&lt;/p&gt;
&lt;p&gt;In a lot of ways though, it has never been more interesting to be alive. Code is easier, learning is easier, diagnosing is easier, boring things are easier. That is progress. But using all of those easier things to build something stupid has also never been easier. And that feels like the opposite of progress.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;I'm an optimist. Where a lot of people see doom, I see something interesting. That’s why I am in love with technology. When changes are this big then fear, uncertainty, and doubt prevail. Keep two things in mind: it’s relevant to understand how AI works to some extent, and  remember that &lt;a href="https://calnewport.com/no-one-knows-anything-about-ai/"&gt;no one knows anything.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When the dust has settled another thing will become clear: taste, creativity, and the will of doing interesting things has never been more important. If I used ChatGPT to write this entire post, it would probably suck. But if I use ChatGPT strategically to give me feedback on how to make it better - it probably will.&lt;/p&gt;
&lt;p&gt;Like Vitto says: There are many fun things to do: go do them.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-common-misconceptions-about-ai-1"&gt;You should try &lt;a href="https://lmstudio.ai/"&gt;LM Studio&lt;/a&gt; &lt;a href="#sf-common-misconceptions-about-ai-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-common-misconceptions-about-ai-2"&gt;Even if they &lt;a href="https://privacy.anthropic.com/en/articles/10023580-is-my-data-used-for-model-training"&gt;use your data&lt;/a&gt; for training - but this loop takes a while - it's not instant &lt;a href="#sf-common-misconceptions-about-ai-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>A Benchmark for language models on European Portuguese</title><link href="https://duarteocarmo.com/blog/a-benchmark-for-language-models-on-european-portuguese.html" rel="alternate"/><published>2025-07-21T00:00:00+02:00</published><updated>2025-07-21T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-07-21:/blog/a-benchmark-for-language-models-on-european-portuguese.html</id><summary type="html">&lt;div class="iframe-container"&gt;
  &lt;iframe scrolling="no" id="euroeval-frame" src="https://duarteocarmo.com/html/pt-euroeval.html" title="EuroEval European Portuguese Benchmarks" loading="lazy"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;

&lt;style&gt;
  .iframe-container {
    width: 100%;
    max-width: 800px;
    margin: 2rem auto;
    text-align: center;
  }

  #euroeval-frame {
    width: 100%;
    height: 500px;
    border: none;
    display: block;
    box-sizing: border-box;
  }

  @media (max-width: 768px) {
    #euroeval-frame {
      height: 450px;
    }
  }

  @media (max-width: 480px) {
    #euroeval-frame {
      height: 350px;
    }
  }
&lt;/style&gt;

&lt;p&gt;A couple of weeks ago in Lisbon, I went to a friend's birthday dinner. In front of …&lt;/p&gt;</summary><content type="html">&lt;div class="iframe-container"&gt;
  &lt;iframe scrolling="no" id="euroeval-frame" src="https://duarteocarmo.com/html/pt-euroeval.html" title="EuroEval European Portuguese Benchmarks" loading="lazy"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;

&lt;style&gt;
  .iframe-container {
    width: 100%;
    max-width: 800px;
    margin: 2rem auto;
    text-align: center;
  }

  #euroeval-frame {
    width: 100%;
    height: 500px;
    border: none;
    display: block;
    box-sizing: border-box;
  }

  @media (max-width: 768px) {
    #euroeval-frame {
      height: 450px;
    }
  }

  @media (max-width: 480px) {
    #euroeval-frame {
      height: 350px;
    }
  }
&lt;/style&gt;

&lt;p&gt;A couple of weeks ago in Lisbon, I went to a friend's birthday dinner. In front of me sat someone that recently started working for the Portuguese government where they focus on modernization and technology. It's not everyday that I talk to someone that works for the Portuguese government in an area similar to mine, so I was very curious. I asked about the &lt;a href="https://www.it.pt/News/NewsPost/5065"&gt;AMÁLIA&lt;/a&gt; project. The 5.5 Million Euro project about creating a new LLM &lt;em&gt;specifically&lt;/em&gt; designed for Portuguese.&lt;/p&gt;
&lt;p&gt;The first beta release of AMÁLIA is scheduled for the &lt;a href="https://www.portugal.gov.pt/pt/gc24/comunicacao/noticia?i=modelo-de-linguagem-em-grande-escala-para-a-lingua-portuguesa"&gt;first trimester of 2025&lt;/a&gt;, but I've heard little news about it. Still I asked: "Are you pre-training it? Or are you fine-tuning something that is already out there?". "No, we're training from scratch." the person told me. "Really, what's the point of that?" I asked, but did not get a clear reply. But the question stuck in my mind. What's the goal? To show Portugal is capable of pre-training a model from scratch? To build a model that is &lt;em&gt;specifically&lt;/em&gt; really good at Portuguese? &lt;sup id="sf-a-benchmark-for-language-models-on-european-portuguese-1-back"&gt;&lt;a href="#sf-a-benchmark-for-language-models-on-european-portuguese-1" class="simple-footnote" title="Other countries like Italy have done this - See Minerva. They also spend 5M, but from EU funds afaik"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id="the-problem-shooting-in-the-dark"&gt;The problem: Shooting in the dark&lt;/h2&gt;
&lt;p&gt;Let's assume the goal is to build a model that is &lt;em&gt;really good&lt;/em&gt; at Portuguese, and that the Portuguese state is really not interested in "showing we are capable of training models" (we have &lt;a href="https://www.theguardian.com/commentisfree/2025/jun/25/lisbon-europe-portugal-golden-visa-capital-investors-short-term-rentals"&gt;bigger problems&lt;/a&gt;). If we assume that, the first thing I would do would actually be to &lt;strong&gt;measure how good language models are at speaking Portuguese&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Portuguese is a popular language, it's the &lt;strong&gt;8th&lt;/strong&gt; most spoken language in the world. But it comes in &lt;a href="https://en.wikipedia.org/wiki/Portuguese-speaking_world"&gt;flavors&lt;/a&gt;. Portuguese spoken in Brazil (82% of native speakers) is quite different from Portuguese from Portugal (4% of speakers). A common complaint from people in Portugal is that the models often reply in "Brazilian" Portuguese. The difference is not only about the pronunciation: a lot of the vocabulary, verbs, and conjugations are different.&lt;/p&gt;
&lt;p&gt;Let's take Llama 3. It &lt;a href="https://scontent-cph2-1.xx.fbcdn.net/v/t39.2365-6/468347782_9231729823505907_4580471254289036098_n.pdf?_nc_cat=110&amp;amp;ccb=1-7&amp;amp;_nc_sid=3c67a6&amp;amp;_nc_ohc=MN9Qsqv_WlwQ7kNvwFzR-jm&amp;amp;_nc_oc=Adl-itXHUl8EqL_TJpicf2-H5wTmlDZO7zwWJkSwPs1eFM7cXlQzA1ddUZSIonQnPxI&amp;amp;_nc_zt=14&amp;amp;_nc_ht=scontent-cph2-1.xx&amp;amp;_nc_gid=r6Z1jsaqV629vM4DSY2Iag&amp;amp;oh=00_AfQPdfe0Ubcu4_kec6ttGpTvvtA2MNJeEJZ7_xzT3SJMFg&amp;amp;oe=6882A3C0"&gt;was trained&lt;/a&gt; on 8% multilingual data. If we assume 2% of that was Portuguese, and if we assume 5% of that is European Portuguese, then only &lt;strong&gt;0.008%&lt;/strong&gt; of the data Llama saw was European Portuguese. That's not a lot. But under representation is a common issue.&lt;/p&gt;
&lt;h2 id="european-portuguese-on-euroeval"&gt;European Portuguese on EuroEval&lt;/h2&gt;
&lt;p&gt;Denmark has a similar problem, and I knew Dan Nielsen at &lt;a href="https://alexandra.dk/"&gt;The Alexandra Institute&lt;/a&gt; worked on something called &lt;a href="https://arxiv.org/pdf/2304.00906"&gt;ScandEval&lt;/a&gt; where he evaluated the performance of language models across different Scandinavian languages. I was surprised to see the project evolved into something more general: &lt;a href="https://euroeval.com/"&gt;EuroEval&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;EuroEval, is similar to ScandEval - but for all European languages. And you want to guess the one that &lt;em&gt;was&lt;/em&gt; missing? Yes, Portuguese.&lt;/p&gt;
&lt;p&gt;Over a couple of weeks, &lt;a href="https://github.com/EuroEval/EuroEval/issues/1040"&gt;we put together&lt;/a&gt;&lt;sup id="sf-a-benchmark-for-language-models-on-european-portuguese-2-back"&gt;&lt;a href="#sf-a-benchmark-for-language-models-on-european-portuguese-2" class="simple-footnote" title="Thanks Dan for all the help in making this happen!"&gt;2&lt;/a&gt;&lt;/sup&gt; a collection of datasets to evaluate the performance of language models in in European Portuguese. We also did some extra work to ensure that the data is &lt;em&gt;exclusively&lt;/em&gt; Portuguese from Portugal.&lt;/p&gt;
&lt;p&gt;Here are the datasets we put together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sentiment Classification&lt;/strong&gt; (&lt;a href="https://huggingface.co/datasets/duarteocarmo/sst2-pt-mini"&gt;SST2-PT&lt;/a&gt;): Part of the work from the &lt;a href="https://arxiv.org/abs/2404.05333"&gt;ExtraGLUE&lt;/a&gt; project. A sentiment analysis dataset built using machine translation (DeepL).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Named Entity Recognition&lt;/strong&gt; (&lt;a href="https://huggingface.co/datasets/duarteocarmo/harem"&gt;HAREM&lt;/a&gt;): Part of the work from the &lt;a href="https://www.linguateca.pt/harem/"&gt;HAREM project&lt;/a&gt;. We filter by entries where the origin is PT - to create an NER dataset.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Linguistic Acceptability&lt;/strong&gt; (&lt;a href="https://huggingface.co/datasets/duarteocarmo/scala-pt"&gt;ScaLA-pt&lt;/a&gt;): Based on &lt;a href="https://universaldependencies.org/treebanks/pt_bosque/index.html"&gt;Portuguese-Bosque treebank&lt;/a&gt;, filtered by entries from &lt;a href="https://www.linguateca.pt/cetempublico/"&gt;CETEMPúblico&lt;/a&gt;. Created by corrupting grammatically correct sentences.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reading Comprehension&lt;/strong&gt; (&lt;a href="https://huggingface.co/datasets/duarteocarmo/boolq-pt"&gt;BoolQ-PT&lt;/a&gt;): Also part of the &lt;a href="https://arxiv.org/abs/2404.05333"&gt;ExtraGlue&lt;/a&gt; work. Adapted by taking the original passage, question, and yes/no options, and turning it into a Q/A style question where the model can answer yes or no.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Knowledge&lt;/strong&gt; (&lt;a href="https://huggingface.co/datasets/duarteocarmo/mmlu-pt-mini"&gt;MMLU-pt&lt;/a&gt;): Based on &lt;a href="https://arxiv.org/abs/2410.08928"&gt;this paper&lt;/a&gt;. Already included entries specifically for Portuguese from Portugal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Common-sense Reasoning&lt;/strong&gt; (&lt;a href="https://huggingface.co/datasets/duarteocarmo/goldenswag-pt-mini"&gt;GoldenSwag-pt&lt;/a&gt;): High quality filtered samples from the &lt;a href="https://aclanthology.org/P19-1472/"&gt;HellaSwag dataset&lt;/a&gt;. Also machine translated with DeepL for European Portuguese.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Summarization&lt;/strong&gt; (&lt;a href="https://huggingface.co/datasets/duarteocarmo/publico-mini"&gt;Publico&lt;/a&gt;): Filtered the &lt;a href="https://commoncrawl.org/blog/news-dataset-available"&gt;CCNews corpus&lt;/a&gt; for entries where the url matched &lt;a href="https://www.publico.pt/"&gt;Público&lt;/a&gt;. Transformed into summarization dataset by extracting the first two sentences as the summary (a common trick).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I you want to look at some samples, &lt;a href="https://huggingface.co/duarteocarmo"&gt;I published the datasets on HuggingFace&lt;/a&gt;. You can also read the extensive descriptions on the &lt;a href="https://euroeval.com/datasets/portuguese/"&gt;EuroEval docs&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="running-benchmarks-for-european-portuguese"&gt;Running benchmarks for European Portuguese&lt;/h2&gt;
&lt;p&gt;While Dan is working on the general leaderboard, I ran some benchmarks on my own which you saw on top of this blog post or &lt;a href="/html/pt-euroeval.html"&gt;in this link&lt;/a&gt;. I selected some models I was curious about within 3 "categories": Large, Small, and things I can run on my laptop without the fan coming on.&lt;/p&gt;
&lt;p&gt;If you're curious about how a particular model performs that I (or Dan) didn't benchmark, you can also run them yourself (assuming you have &lt;a href="https://docs.astral.sh/uv/getting-started/installation/"&gt;uv&lt;/a&gt; installed):&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;uvx&lt;span class="w"&gt; &lt;/span&gt;--with&lt;span class="w"&gt; &lt;/span&gt;euroeval&lt;span class="w"&gt; &lt;/span&gt;euroeval&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--model&lt;span class="w"&gt; &lt;/span&gt;ollama_chat/smollm2:135m&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--task&lt;span class="w"&gt; &lt;/span&gt;sentiment-classification&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--language&lt;span class="w"&gt; &lt;/span&gt;pt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Set the &lt;code&gt;--model&lt;/code&gt; flag to any model that &lt;a href="https://docs.litellm.ai/docs/providers"&gt;LiteLLM&lt;/a&gt; supports.&lt;/p&gt;
&lt;p&gt;The reality of building these benchmarks is a clear realization: European Portuguese is an unpopular language. Brazilian Portuguese is simply a much more popular version of the language. Still, there is value in building benchmarks focused on the European variant. And it was a lot of fun tracking/building these datasets. I expect to do some more work in this area.&lt;/p&gt;
&lt;p&gt;And I &lt;em&gt;don't&lt;/em&gt; know if investing 5.5M Euro in developing a Portuguese Language Model is a good idea. But there's one thing I do know: Whenever that model comes out, we are now in a &lt;em&gt;much&lt;/em&gt; better position to say if it hit the mark. Or if it didn't.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Note: The benchmarks above are preliminary. For the official ones keep an eye on &lt;a href="https://euroeval.com/leaderboards/"&gt;EuroEval Leaderboards&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-a-benchmark-for-language-models-on-european-portuguese-1"&gt;Other countries like Italy have done this - &lt;a href="https://minerva-ai.org/"&gt;See Minerva&lt;/a&gt;. They also spend 5M, &lt;a href="https://eurohpc-ju.europa.eu/advancing-ai-eurohpc-minerva-project-2025-02-13_en"&gt;but from EU funds&lt;/a&gt; afaik &lt;a href="#sf-a-benchmark-for-language-models-on-european-portuguese-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-a-benchmark-for-language-models-on-european-portuguese-2"&gt;Thanks &lt;a href="https://www.saattrupdan.com/"&gt;Dan&lt;/a&gt; for all the help in making this happen! &lt;a href="#sf-a-benchmark-for-language-models-on-european-portuguese-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>MCPs are mostly hype</title><link href="https://duarteocarmo.com/blog/mcps-are-mostly-hype.html" rel="alternate"/><published>2025-06-06T00:00:00+02:00</published><updated>2025-06-06T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-06-06:/blog/mcps-are-mostly-hype.html</id><summary type="html">&lt;p&gt;..but they can also be a lot of fun. If you work in tech, I'd say there's a 98% chance you've heard about it. MCPs are the future of agents, MCPs will be everywhere, MCPs are the future. The Model Context Protocol, first introduced by Anthropic is blowing up. For …&lt;/p&gt;</summary><content type="html">&lt;p&gt;..but they can also be a lot of fun. If you work in tech, I'd say there's a 98% chance you've heard about it. MCPs are the future of agents, MCPs will be everywhere, MCPs are the future. The Model Context Protocol, first introduced by Anthropic is blowing up. For both &lt;a href="https://modelcontextprotocol.io/introduction#why-mcp%3F"&gt;good&lt;/a&gt; and &lt;a href="https://simonwillison.net/2025/Apr/9/mcp-prompt-injection/#mixing-tools-with-untrusted-instructions-is-inherently-dangerous"&gt;bad&lt;/a&gt; reasons.&lt;/p&gt;
&lt;p&gt;For me, as always, I refrain from judgement before taking anything for a spin. At the end of the day, MCP is a standardized way of giving access to tools to an LLM. Those tools come in the form of function calling. So nothing groundbreaking.&lt;/p&gt;
&lt;p&gt;I've tested a few MCP servers. Adding the Gitlab MCP server to Zen, adding the Polars docs MCP to Claude, adding a TickTick (my todo list) MCP server to Bolt. They all worked &lt;em&gt;sometimes&lt;/em&gt;. And in computers, when something works sometimes, well - I prefer using something else.&lt;/p&gt;
&lt;p&gt;As the training for Oslo ramps up, I've once again felt the need to give an LLM access to "the outside world".&lt;/p&gt;
&lt;h3 id="accessing-my-training-textbooks"&gt;Accessing my training textbooks&lt;/h3&gt;
&lt;p&gt;For Oslo (and for the past 3 years) - I've been following Phitzinger's training methodology. In short, I believe mostly in volume. I have the pdf version of the book, it's great. Whenever I dropped the book into any of the LLM/Chat apps, I almost always got the same "context is full" or "too many tokens within X mins" message.&lt;/p&gt;
&lt;p&gt;I don't want the LLM to access the whole book. I want it to access some parts of the book. If your mind jumps straight to RAG, let me stop you right there. It's not RAG, it's search. We don't want a whole vector database. We don't want a whole retrieval system. We want keyword search, over my book. That's it.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;prep_book&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BM25Okapi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;script_dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;
    &lt;span class="c1"&gt;# uvx --from &amp;quot;markitdown[pdf]&amp;quot; markitdown marathon.pdf &amp;gt; marathon.md (make it a md file)&lt;/span&gt;
    &lt;span class="n"&gt;book&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;script_dir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;marathon.md&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;r&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;utf-8&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;corpus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;chunk_size_words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;
    &lt;span class="n"&gt;overlap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_size_words&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;chunk_size_words&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;corpus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;chunk_size_words&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

    &lt;span class="n"&gt;tokenized_corpus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;corpus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;bm25&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BM25Okapi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenized_corpus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bm25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;corpus&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;search_marathon_training_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Search the marathon training book for a specific query.&lt;/span&gt;
&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        query (str): The search query.&lt;/span&gt;
&lt;span class="sd"&gt;        total_results (int): The number of results to return. (default: 10)&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;bm25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;corpus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prep_book&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;tokenized_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bm25&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_top_n&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenized_query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;corpus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;total_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results_as_bullets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;lt;result_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&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="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/result_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&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="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;lt;search_marathon_training_book&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;lt;search_query&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/search_query&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;results_as_bullets&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;lt;/search_marathon_training_book&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="accessing-my-training-plan"&gt;Accessing my training plan&lt;/h3&gt;
&lt;p&gt;Now that we have a function to access my training "philosophy", we also need the LLM to be able to access my training plan. I'm a simple guy, &lt;a href="https://defy.org/hacks/calendarhack"&gt;I like my training plan in my calendar&lt;/a&gt;. It's one less tool I need to use. Well - we can also give the LLM access to my calendar's &lt;code&gt;ics&lt;/code&gt; file. We can just call the &lt;code&gt;get_training_program_events&lt;/code&gt; and access the training events within a time range. Simple, effective.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_training_program_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Fetches the athlete&amp;#39;s training program events in the date range specified.&lt;/span&gt;

&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        start_date: Start date in format &amp;quot;DD-MM-YYYY&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;        end_date: End date in format &amp;quot;DD-MM-YYYY&amp;quot;&lt;/span&gt;

&lt;span class="sd"&gt;    Returns:&lt;/span&gt;
&lt;span class="sd"&gt;        A string with matching VEVENTS in XML-like format.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;-%m-%Y&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;UTC&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;-%m-%Y&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;UTC&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ics_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;oslo_training_plan.ics&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;ics_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ics_path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;cal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_ical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ics_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;events_xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;UTC&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;UTC&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;walk&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;VEVENT&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;dtstart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DTSTART&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;dtend_raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DTEND&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;dtend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DTEND&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dtend_raw&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dtend&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;dtstart&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;dtend&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;SUMMARY&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;LOCATION&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;events_xml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&amp;lt;event&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;  &amp;lt;summary&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;  &amp;lt;location&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/location&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;  &amp;lt;start&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dtstart&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/start&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;  &amp;lt;end&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dtend&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/end&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;&amp;lt;/event&amp;gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;lt;events&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events_xml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/events&amp;gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="checking-my-runs"&gt;Checking my runs&lt;/h3&gt;
&lt;p&gt;The LLM also needs to be able to compare the planned training sessions to the actual runs I've made (you know - to keep me in check, or give me feedback). The easiest (NOT the safest) way to do this is to use the &lt;code&gt;garminconnect&lt;/code&gt; library. We can also wrap that up in a simple function:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;fetch_athlete_runs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lookback_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Fetch athlete&amp;#39;s running activities from Garmin Connect.&lt;/span&gt;
&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        lookback_days (int): Number of days to look back for running activities.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;garmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;start_garmin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;startdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lookback_days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;activities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;garmin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_activities_by_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;startdate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;activitytype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;running&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sorted_runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;activities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;startTimeLocal&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;lt;Runs&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sorted_runs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;startTimeLocal&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;distance_km&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;distance&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;duration_sec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;duration&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;duration_fmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duration_sec&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;avg_hr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;averageHR&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;n/a&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pace_sec_per_km&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;duration_sec&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;distance_km&lt;/span&gt;
        &lt;span class="n"&gt;pace_min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pace_sec_per_km&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pace_sec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pace_sec_per_km&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;pace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pace_min&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pace_sec&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;02d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; min/km&amp;quot;&lt;/span&gt;

        &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;  &amp;lt;Run&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;lt;Date&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/Date&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;lt;Distance&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;distance_km&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; km&amp;lt;/Distance&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;lt;Duration&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;duration_fmt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/Duration&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;lt;Pace&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pace&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/Pace&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;lt;HeartRate&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;avg_hr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; bpm&amp;lt;/HeartRate&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;  &amp;lt;/Run&amp;gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;lt;/Runs&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="putting-it-all-together"&gt;Putting it all together&lt;/h3&gt;
&lt;p&gt;Our goal would be to give an LLM access to all these tools at the same time, and let it analyze and give me feedback on my training. MCP is supposed to solve just that - putting all your tools in a single place. This is the moment where we would start developing our complicated 12-factor MCP architecture. But instead of doing that, why don't we just use uv scripts?&lt;/p&gt;
&lt;p&gt;The only thing to do, is to drop all the functions we want to give an LLM in a single file. With that in place, we can run &lt;code&gt;uv run oslo_marathon_mcp.py&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# /// script&lt;/span&gt;

&lt;span class="c1"&gt;# requires-python = &amp;quot;&amp;gt;=3.12&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# dependencies = [&lt;/span&gt;

&lt;span class="c1"&gt;#     &amp;quot;mcp[cli]&amp;quot;,&lt;/span&gt;

&lt;span class="c1"&gt;#     &amp;quot;garminconnect&amp;quot;,&lt;/span&gt;

&lt;span class="c1"&gt;#     &amp;quot;rank-bm25&amp;quot;,&lt;/span&gt;

&lt;span class="c1"&gt;#     &amp;quot;icalendar&amp;quot;,&lt;/span&gt;

&lt;span class="c1"&gt;# ]&lt;/span&gt;

&lt;span class="c1"&gt;# ///&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;mcp.server.fastmcp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;rank_bm25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BM25Okapi&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;garminconnect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Garmin&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pathlib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;icalendar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Calendar&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;zoneinfo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ZoneInfo&lt;/span&gt;

&lt;span class="n"&gt;MCP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;oslo_marathon&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@MCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;search_marathon_training_book&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Search the marathon training book for a specific query.&lt;/span&gt;
&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        query (str): The search query.&lt;/span&gt;
&lt;span class="sd"&gt;        total_results (int): The number of results to return. (default: 10)&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@MCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;fetch_athlete_runs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lookback_days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Fetch athlete&amp;#39;s running activities from Garmin Connect.&lt;/span&gt;
&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        lookback_days (int): Number of days to look back for running activities.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@MCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_training_program_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Fetches the athlete&amp;#39;s training program events in the date range specified.&lt;/span&gt;

&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        start_date: Start date in format &amp;quot;DD-MM-YYYY&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;        end_date: End date in format &amp;quot;DD-MM-YYYY&amp;quot;&lt;/span&gt;

&lt;span class="sd"&gt;    Returns:&lt;/span&gt;
&lt;span class="sd"&gt;        A string with matching VEVENTS in XML-like format.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@MCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_current_date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Returns the current date in ISO format.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;UTC&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;MCP&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;stdio&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/duarteocarmo/8f1463500e0c843b6e7848e0b5466ecc"&gt;Entire file here.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Our MCP is ready. There are just short of 8984 ways to use an MCP server with an LLM. My favorite one is &lt;em&gt;still&lt;/em&gt; BoltAI. We open our MCP server configuration and add our nice little MCP server:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;mcpServers&amp;quot;&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="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;oslo_marathon&amp;quot;&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="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;args&amp;quot;&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="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;run&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/Users/duarteocarmo/Repos/scripts/oslo_marathon_mcp.py&amp;quot;&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="nt"&gt;&amp;quot;command&amp;quot;&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="s2"&gt;&amp;quot;uv&amp;quot;&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;One of the nice features of BoltAI, is the ability to create "projects". These are similar to the &lt;a href="https://help.openai.com/en/articles/10169521-using-projects-in-chatgpt"&gt;projects&lt;/a&gt; feature of ChatGPT. In short, BoltAI gives you the ability to create a template that will be used for a group of chats. I created a "Running" project where I defined the model I'd like to use, as well as the &lt;code&gt;oslo_marathon&lt;/code&gt; MCP server. BoltAI also gives you the ability to investigate the MCP server to quickly check what tools are made available to the LLM.&lt;/p&gt;
&lt;p&gt;&lt;img alt="bolt mcp server" src="https://duarteocarmo.com/images/81/mcp_bolt.webp" /&gt;&lt;/p&gt;
&lt;p&gt;Finally, once everything is configured, we can now ask the LLM something very specific according to my run. And as you can see, the LLM will call all the necessary tools in order. Just like the &lt;a href="https://arxiv.org/abs/2210.03629"&gt;ReAct paper&lt;/a&gt; introduced - the LLM now gives us much more contextualized information about my run, and searches the Pfitzinger training book for information on recovering strategies.&lt;/p&gt;
&lt;p&gt;&lt;img alt="bolt mcp calls" src="https://duarteocarmo.com/images/81/chat.webp" /&gt;&lt;/p&gt;
&lt;h3 id="final-thoughts"&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;For the past months, I've seen many people talking about MCP (and even &lt;a href="https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/"&gt;A2A&lt;/a&gt;) as things that will revolutionize AI and how we interact with LLMs. As always, there's A LOT of claims out there about what these protocols can do, and the promise they carry. It's hype and over-excitement in its purest form.&lt;/p&gt;
&lt;p&gt;I can't help but cringe sometimes when I see a big tech CTO talking about their incredibly great MCP server. For the rest of us that work in the industry. MCP is &lt;em&gt;just&lt;/em&gt; a list of functions we give the LLM access to, wrapped in a FastAPI server.&lt;/p&gt;
&lt;p&gt;And we can't deny. Tool calling is &lt;em&gt;incredibly&lt;/em&gt; powerful, but it's &lt;em&gt;still&lt;/em&gt; tool calling. Tools should be targeted, well described, and made easy to understand by the Large Language Model. It's not about throwing a hammer around in a dark room and hoping that it will pick up an API call that was designed for programmers - it's about giving LLMs the right access in a controlled manner.&lt;/p&gt;
&lt;p&gt;Don't be fooled. You don't need to download one of the &lt;a href="https://mcp.so/"&gt;thousands&lt;/a&gt; of MCP servers of &lt;a href="https://simonwillison.net/2025/Apr/9/mcp-prompt-injection/#mixing-tools-with-untrusted-instructions-is-inherently-dangerous"&gt;dubious&lt;/a&gt; quality and security. You can just create a &lt;a href="https://gist.github.com/duarteocarmo/8f1463500e0c843b6e7848e0b5466ecc"&gt;200 line Python file&lt;/a&gt; and give it to the LLM, it has the same (probably better) effect.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Evals are all you need</title><link href="https://duarteocarmo.com/blog/evals-are-all-you-need.html" rel="alternate"/><published>2025-05-04T00:00:00+02:00</published><updated>2025-05-04T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-05-04:/blog/evals-are-all-you-need.html</id><summary type="html">&lt;p&gt;Last month, I built an app called &lt;a href="https://duarteocarmo.com/blog/taralli-home-cooked-food-tracking-without-the-bs.html"&gt;Taralli&lt;/a&gt;. It was fun to close the loop and get it out there. Still - there was an elephant in the room:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"...[calorie tracking] Accuracy isn’t great, and it makes some pretty basic mistakes. I’ve got plans to fix that..."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And things …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Last month, I built an app called &lt;a href="https://duarteocarmo.com/blog/taralli-home-cooked-food-tracking-without-the-bs.html"&gt;Taralli&lt;/a&gt;. It was fun to close the loop and get it out there. Still - there was an elephant in the room:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"...[calorie tracking] Accuracy isn’t great, and it makes some pretty basic mistakes. I’ve got plans to fix that..."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And things started failing pretty early. &lt;code&gt;100g of peanut butter&lt;/code&gt; came back with 58 000 kcal - &lt;code&gt;350g of pasta&lt;/code&gt; as 60 000 kcal - quantities were all over the place. And even though most users were giving me good feedback - It was time to fix things. After some work, I managed to improve tracking accuracy from &lt;strong&gt;17% to 76%&lt;/strong&gt;. It turns out, evals are all you need.&lt;/p&gt;
&lt;h2 id="naive-is-good-but-it-fails-eventually"&gt;Naive is good, but it fails eventually&lt;/h2&gt;
&lt;p&gt;I constantly preach about how we shouldn't build complicated things before knowing if they're &lt;em&gt;actually&lt;/em&gt; going to be used. Taralli's first system for calorie counting was embarrassingly simple. All it did was take a user's food description as a string - pass it through &lt;code&gt;gpt-4o-mini&lt;/code&gt; with &lt;a href="https://platform.openai.com/docs/guides/structured-outputs"&gt;structured outputs&lt;/a&gt;, and generate a JSON response. Here's the &lt;a href="https://docs.pydantic.dev/latest/"&gt;Pydantic&lt;/a&gt; model that gives that JSON format:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FoodGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dairy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;dairy&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;meat_and_alternatives&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;meat and alternatives&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;grain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;grain&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;fruit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;fruit&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;vegetable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;vegetable&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FoodItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;The name of the food item&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;The quantity of the food item in the user&amp;#39;s description&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;calories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Calories in kilocalories (kcal) for a single unit of the food item&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;carbs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Carbohydrates in grams for a single unit of the food item&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Fat in grams for a single unit of the food item&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;protein&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Protein in grams for a single unit of the food item&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Fiber in grams for a single unit of the food item&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;food_groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FoodGroup&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;The food groups to which the food item belongs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;NutritionAnalysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;food_items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FoodItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;A list of food items with their nutritional information&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With this in place - it's relatively simple to build an API that does the following:&lt;/p&gt;
&lt;p&gt;Input: &lt;code&gt;a handful of peanuts&lt;/code&gt;, output:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;food_items&amp;quot;&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="nt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;peanuts&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;quantity&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;calories&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;414.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;carbs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;16.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;fat&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;36.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;protein&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;19.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;fiber&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;8.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;food_groups&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:[&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;meat and alternatives&amp;quot;&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="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The system returns not only calories but also macronutrients (carbs, fat, protein, fiber, and food groups). And even though the model respected the format almost 100% of the time, cracks started showing pretty quickly:&lt;/p&gt;
&lt;p&gt;Input: &lt;code&gt;100g of peanut butter&lt;/code&gt;, output:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;food_items&amp;quot;&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="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;peanut butter&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;quantity&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;calories&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;588.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;carbs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;20.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;fat&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;50.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;protein&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;25.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;fiber&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;food_groups&amp;quot;&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="s2"&gt;&amp;quot;meat and alternatives&amp;quot;&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="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Wait a second, 100 x 588? That’s 58,800 calories for 100g of peanut butter. That’s &lt;em&gt;wildly&lt;/em&gt; off (the model is &lt;a href="https://www.google.com/search?q=calories+in+100g+of+peanut+butter&amp;amp;oq=calories+in+100g+of+pean&amp;amp;gs_lcrp=EgZjaHJvbWUqBwgCEAAYgAQyBggAEEUYOTIHCAEQABiABDIHCAIQABiABDIICAMQABgWGB4yCAgEEAAYFhgeMggIBRAAGBYYHjIICAYQABgWGB4yCAgHEAAYFhgeMggICBAAGBYYHjIICAkQABgWGB7SAQgzOTc0ajFqN6gCALACAA&amp;amp;sourceid=chrome&amp;amp;ie=UTF-8"&gt;misinterpreting&lt;/a&gt; what I want for quantity here). It should have set name to "100g of peanut butter" and quantity to 1. For example.&lt;/p&gt;
&lt;h2 id="creating-a-golden-dataset"&gt;Creating a golden dataset&lt;/h2&gt;
&lt;p&gt;When I published Taralli, I made the app completely free (even though I was paying for every LLM call). I did warn users that I was logging all of their inputs into the food tracking model. Using &lt;a href="https://wandb.ai/site/weave/"&gt;W&amp;amp;B Weave&lt;/a&gt; and a simple decorator, I was able to log every input-output of the system.&lt;/p&gt;
&lt;p&gt;&lt;img alt="weave screenshot" src="https://duarteocarmo.com/images/80/weave.png" /&gt;&lt;/p&gt;
&lt;p&gt;That allowed me to start capturing real-world examples of where the prompt was failing. Looking through the data, I found several cases where my zero-shot prompt was just not cutting it (wrong food groups, lots of wrong quantities). Using this data, I started collecting a &lt;em&gt;golden dataset&lt;/em&gt;: a collection of food descriptions and their corresponding nutritional analysis. Using OpenAI's o3 and Google's Gemini 2.5 Pro - I was able to make sure all the quantities, descriptions, and food groups were exactly like I wanted them.&lt;/p&gt;
&lt;p&gt;But I still wanted a way to visualize, edit and update the dataset. So I built a small visualizer where I could see everything that was going on, and edit whatever was wrong. 200% vibe coded.&lt;/p&gt;
&lt;p&gt;&lt;img alt="dataset_reviewer" src="https://duarteocarmo.com/images/80/reviewer.png" /&gt;&lt;/p&gt;
&lt;h2 id="a-simple-metric"&gt;A simple metric&lt;/h2&gt;
&lt;p&gt;With my golden dataset in place, I needed a way of evaluating whether a given prediction from the prompt was &lt;em&gt;good&lt;/em&gt; or &lt;em&gt;bad&lt;/em&gt;. By looking at my entries - the most common mistakes were pretty obvious. Either it was calorie totals - or missing food groups.&lt;/p&gt;
&lt;p&gt;My approach was very basic (again) - and could be even more refined. Given a certain food description:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Is the total calories of the prediction within 10% of the total calories of the golden example?&lt;/li&gt;
&lt;li&gt;Is there an overlap between the food groups of the golden example vs. the predicted one?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the answer to &lt;strong&gt;both&lt;/strong&gt; questions is 'Yes' - then the prediction is good. In code, that roughly translates to:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;eval_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;gold_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;food_items&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;pred_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;food_items&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_totals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
        &lt;span class="n"&gt;calories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;calories&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;quantity&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;union&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;food_groups&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;calories&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;

    &lt;span class="n"&gt;gold_cal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gold_groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_totals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold_items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pred_cal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pred_groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_totals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pred_items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;calories_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold_cal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pred_cal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel_tol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gold_groups&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;pred_groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# at least one side has groups&lt;/span&gt;
        &lt;span class="n"&gt;groups_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gold_groups&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;pred_groups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;groups_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# both lists empty&lt;/span&gt;

    &lt;span class="n"&gt;sum_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calories_ok&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;groups_ok&lt;/span&gt;
    &lt;span class="n"&gt;is_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sum_ok&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="c1"&gt;# if both are ok&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;is_ok&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now that we have a metric - we can make number go up.&lt;/p&gt;
&lt;h2 id="using-dspy-to-improve-the-model"&gt;Using DSPy to Improve the Model&lt;/h2&gt;
&lt;p&gt;The first step was to use DSPy, &lt;a href="https://duarteocarmo.com/blog/what-the-hell-is-gqpa-anyway.html"&gt;a framework I've talked about before&lt;/a&gt;, to evaluate our &lt;em&gt;current&lt;/em&gt; performance. After splitting my golden dataset into training and testing splits, and I had my metric ready, I could easily assess how well my current system was performing:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;process_example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val_example&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;classify_food&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val_example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;food_description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;eval_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val_example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process_example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val_set&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which resulted in:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;=====EVALUATION=====
example.food_description=&amp;#39;a banana&amp;#39;
gold_cal=105
pred_cal=105
CALORIES_OK? True
gold_groups={&amp;#39;fruit&amp;#39;}
pred_groups={&amp;lt;FoodGroup.fruit: &amp;#39;fruit&amp;#39;&amp;gt;}
GROUPS_OK? True
is_ok=True
=====EVALUATION=====
example.food_description=&amp;#39;300 g stew with chicken, potatoes and vegetables&amp;#39;
gold_cal=270
pred_cal=360
CALORIES_OK? False
gold_groups={&amp;#39;vegetable&amp;#39;, &amp;#39;meat and alternatives&amp;#39;}
pred_groups={&amp;lt;FoodGroup.grain: &amp;#39;grain&amp;#39;&amp;gt;, &amp;lt;FoodGroup.vegetable: &amp;#39;vegetable&amp;#39;&amp;gt;, &amp;lt;FoodGroup.meat_and_alternatives: &amp;#39;meat and alternatives&amp;#39;&amp;gt;}
GROUPS_OK? True
is_ok=False

# more examples, truncated for brevity..

Score: 17.24%
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The score of my production system on the dataset was only about &lt;strong&gt;17%&lt;/strong&gt;, not great - we know. To get yet another baseline for my system, I first tried using &lt;code&gt;Gemini 2.5 Flash&lt;/code&gt;, as I wanted to keep costs reasonable while still getting better performance. I evaluated it on the dataset using DSPy:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;classify&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NutritionAnalysis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gemini_flash_preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;openrouter/google/gemini-2.5-flash-preview:nitro&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gemini_flash_preview&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;evaluator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;devset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;val_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;num_threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;display_progress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;display_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trainset&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;evaluator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;eval_metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Average Metric: 7.00 / 28 (25.0%): 100%|██████████| 29/29 [00:00&amp;lt;00:00, 960.39it/s]&lt;/span&gt;

&lt;span class="c1"&gt;# 2025/04/30 19:46:41 INFO dspy.evaluate.evaluate: Average Metric: 7.0 / 29 (24.1%)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Using this zero-shot approach, the score was about 25% - better than my &lt;code&gt;GPT-4o mini&lt;/code&gt; implementation, but still far from acceptable for users.&lt;/p&gt;
&lt;p&gt;I decided to try another &lt;code&gt;DSPy&lt;/code&gt; optimization technique called &lt;code&gt;BootstrapFewShotWithRandomSearch&lt;/code&gt;. This approach finds optimal examples from the training dataset to include in the prompt, which can dramatically improve performance. Here's the code:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gemini_flash_preview&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BootstrapFewShotWithRandomSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;eval_metric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_labeled_demos&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_candidate_programs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bootstrap_fewshot_random_search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;classify&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deepcopy&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;trainset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;trainset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;valset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;val_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# final evaluation&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gemini_flash_preview&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;evaluator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;devset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;val_set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;num_threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;display_progress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;display_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trainset&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;evaluator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bootstrap_fewshot_random_search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;eval_metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Average Metric: 22.00 / 29 (75.9%): 100%|██████████| 29/29 [00:00&amp;lt;00:00, 3096.37it/s]&lt;/span&gt;

&lt;span class="c1"&gt;# 2025/04/30 19:46:45 INFO dspy.evaluate.evaluate: Average Metric: 22 / 29 (75.9%)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After a few minutes of optimization and testing, we achieved 76% accuracy on our validation dataset - an incredible improvement over both our previous system and the zero-shot approach.&lt;/p&gt;
&lt;p&gt;There is a trade-off: the optimized prompt includes several few-shot examples, which can impact response time. However, since we're using the relatively fast &lt;code&gt;Gemini 2.5 Flash&lt;/code&gt; model, it doesn't matter &lt;em&gt;that&lt;/em&gt; much.&lt;/p&gt;
&lt;p&gt;We can see that for our problematic example of "100 g of peanut butter," the new model now provides a much more reasonable result:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;gemini_flash_preview&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bootstrap_fewshot_random_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;100 g of peanut butter&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Prediction(food_items=[FoodItem(name=&amp;#39;Peanut butter (100g)&amp;#39;, quantity=1.0, calories=588.0, carbs=20.0, fat=50.0, protein=25.0, fiber=6.0, food_groups=[&amp;#39;meat and alternatives&amp;#39;])])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here's &lt;a href="https://gist.github.com/duarteocarmo/83487a0da408c2470c9b23b257e66931"&gt;the full prompt&lt;/a&gt; that DSPy uses for the example above. As you can see, it's essentially a few-shot approach to solving the problem, and it works surprisingly well.&lt;/p&gt;
&lt;h2 id="integrating-the-improved-model"&gt;Integrating the Improved Model&lt;/h2&gt;
&lt;p&gt;Now that we have an optimized prompt that performs well, we can save it and integrate it into our &lt;code&gt;FastAPI&lt;/code&gt; application:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# save our dspy program&lt;/span&gt;

&lt;span class="n"&gt;PATH_TO_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;../optimized_prompts/bootstrap_fewshot_random_search.json&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;bootstrap_fewshot_random_search&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PATH_TO_PROMPT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# create a function to process food descriptions&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_classifier_async&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Predict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;lm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LM_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;async_max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dspy_program&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NutritionAnalysis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dspy_program&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PATH_TO_PROMPT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dspy_program&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asyncify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dspy_program&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dspy_program&lt;/span&gt;

&lt;span class="c1"&gt;# create an async processor&lt;/span&gt;

&lt;span class="n"&gt;DSPY_PROGRAM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_classifier_async&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nd"&gt;@weave&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_calories_for_dspy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;NutritionAnalysis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Getting calories for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;food&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;DSPY_PROGRAM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;food&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;food_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;food_items&lt;/span&gt;
    &lt;span class="n"&gt;food_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;food_items&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;parsed_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NutritionAnalysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;food_items&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;food_items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Got response: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;parsed_message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NutritionAnalysis&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Expected NutritionAnalysis response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parsed_message&lt;/span&gt;

&lt;span class="c1"&gt;# integrate it into our fastapi application&lt;/span&gt;

&lt;span class="nd"&gt;@app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;/calories-v2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NutritionAnalysis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Get calories for a food item from request body using DSPy.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_calories_v2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FoodRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Get calories for a food item from request body using DSPy.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Invalid or missing API Key&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;get_calories_for_dspy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;food_description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Along with some other new features in &lt;code&gt;Taralli&lt;/code&gt;, I integrated this new API endpoint, and now all users of the app benefit from our improved food prediction model, which is much more accurate and an overall better experience. Still room for improvement though!&lt;/p&gt;
&lt;h2 id="the-flywheel-effect"&gt;The Flywheel Effect&lt;/h2&gt;
&lt;p&gt;What I like about this approach is that it creates a flywheel effect. As more users interact with the app, I collect more data, I can correct/review that data and add it to my golden dataset. This flywheel allows me to continuously update the prompt as we go.&lt;/p&gt;
&lt;p&gt;&lt;img alt="flywheel of continuous improvement" src="https://duarteocarmo.com/images/80/flywheel.png" /&gt;&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;I've seen many projects in the wild just using a prompt or model and leaving it as is. Without a good idea of how good (or probably bad) it really is. My first step - is - as always - just put it out there. There are just too many unknowns to get stuck on the 0 to 1 stage.&lt;/p&gt;
&lt;p&gt;Once things are out there, then it's time to start asking yourself: OK - what does good look like? With some simple monitoring, you'll start understanding what REALLY matters. Once that's done - then - &lt;strong&gt;evaluations are truly all you need&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Also - check out &lt;a href="https://apps.apple.com/dk/app/taralli/id6743634022"&gt;Taralli&lt;/a&gt; - and let me know what you think!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Taralli in action" src="https://duarteocarmo.com/images/80/taralli.webp" /&gt;&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Taralli: Home-Cooked Food Tracking Without the BS</title><link href="https://duarteocarmo.com/blog/taralli-home-cooked-food-tracking-without-the-bs.html" rel="alternate"/><published>2025-04-07T00:00:00+02:00</published><updated>2025-04-07T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-04-07:/blog/taralli-home-cooked-food-tracking-without-the-bs.html</id><summary type="html">&lt;p&gt;For years, I never really cared about &lt;em&gt;what&lt;/em&gt; I ate, &lt;em&gt;how much&lt;/em&gt; I ate, or &lt;em&gt;when&lt;/em&gt; I ate it.&lt;/p&gt;
&lt;p&gt;But sometime late last year, I finally decided to listen to Vitto. I started noticing that what I eat actually had an impact on how much I ran, how well I …&lt;/p&gt;</summary><content type="html">&lt;p&gt;For years, I never really cared about &lt;em&gt;what&lt;/em&gt; I ate, &lt;em&gt;how much&lt;/em&gt; I ate, or &lt;em&gt;when&lt;/em&gt; I ate it.&lt;/p&gt;
&lt;p&gt;But sometime late last year, I finally decided to listen to Vitto. I started noticing that what I eat actually had an impact on how much I ran, how well I slept - and generally, how good I felt. So I decided to do what most people (at a certain point) do: I downloaded &lt;a href="https://www.myfitnesspal.com/"&gt;the&lt;/a&gt; &lt;a href="https://macrofactorapp.com/macrofactor/"&gt;usual&lt;/a&gt; &lt;a href="https://lifesum.com/"&gt;suspects&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Frustration quickly followed. I don't want the pizza from Trader Joe's, I want frozen pizza from &lt;a href="https://www.pingodoce.pt/"&gt;Pingo Doce&lt;/a&gt;. I'm not eating the Doritos sold in the American supermarket, I just ate the healthy ones from &lt;a href="https://kvickly.coop.dk/"&gt;Kvickly&lt;/a&gt;. I want to track two handfuls of peanuts. I want to track a &lt;em&gt;pão de deus com queijo&lt;/em&gt; from the &lt;a href="https://apadariaportuguesa.pt/"&gt;Padaria Portuguesa&lt;/a&gt;. With the usual suspects, it was painful. Everything was either too specific, too general, or matched a food database I didn't care about.&lt;/p&gt;
&lt;p&gt;After talking with Nuno and a couple of other friends, I quickly realized they weren't actually taking photos, scanning barcodes, or meticulously entering calories and macros in these apps. They were asking ChatGPT. That made sense. For some - certainly for me, it's actually more important to track &lt;em&gt;something&lt;/em&gt; than to be extremely accurate. Perfect is the enemy of good. It still holds.&lt;/p&gt;
&lt;p&gt;Enter &lt;a href="https://apps.apple.com/dk/app/taralli/id6743634022"&gt;Taralli&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="taralli app screenshots" src="https://duarteocarmo.com/images/79/taralli-screenshot-food.webp" /&gt;&lt;/p&gt;
&lt;p&gt;Taralli allows you to track &lt;em&gt;anything&lt;/em&gt; you eat (really!). A &lt;a href="https://www.pingodoce.pt/receitas/pao-com-chourico/"&gt;pão com chouriço&lt;/a&gt;, a &lt;a href="https://encomendas.apadariaportuguesa.pt/produto/pao-de-deus/"&gt;pão de Deus&lt;/a&gt;, half a handful of peanuts – whatever language, whatever text, whatever you're eating, Taralli can handle it. Just type what you ate, and it'll handle the rest. It gives you macros, calories, fiber, and food group. It also has a bunch of analytics from the past week so you can see what's going on at a high level. You can also connect it to Apple Health to track your weight (if you're interested in that sort of stuff.)&lt;/p&gt;
&lt;p&gt;Taralli is built using &lt;a href="https://developer.apple.com/xcode/swiftui/"&gt;SwiftUI&lt;/a&gt;. What would have taken me 3-4 months and a lot of googling about SwiftUI took me something like a couple of weeks and the help of Claude 3.7 Sonnet (and still a lot of googling about SwiftUI I have to admit). I'm not sure I would say the whole thing was &lt;a href="https://x.com/karpathy/status/1886192184808149383?lang=en"&gt;"vibe coded"&lt;/a&gt;. I mean, I &lt;em&gt;still&lt;/em&gt; found some bugs, I &lt;em&gt;still&lt;/em&gt; had to make some important decisions about how things work together and are architected - otherwise this app would be a mess. But I have to admit that building the whole thing is way easier now. Except the Apple review process, I can confirm that one is still painful.&lt;/p&gt;
&lt;p&gt;For estimating the calories I'm using &lt;code&gt;gpt-4o-mini&lt;/code&gt; with some &lt;a href="https://platform.openai.com/docs/guides/structured-outputs"&gt;structured generation&lt;/a&gt; served over FastAPI. It actually sucks right now. Accuracy isn’t great, and it makes some pretty basic mistakes. I’ve got plans to fix that — but I’ll leave them for another release. &lt;a href="https://duarteocarmo.com/blog/simple-software"&gt;Remember&lt;/a&gt;, first make it work, &lt;em&gt;then&lt;/em&gt; make it pretty.&lt;/p&gt;
&lt;p&gt;&lt;img alt="taralli app screenshots" src="https://duarteocarmo.com/images/79/taralli-screenshot-graphs.webp" /&gt;&lt;/p&gt;
&lt;p&gt;You'll notice that unlike other apps, Taralli doesn't tell you what to do. That's because I don't care what you do. The goal of Taralli is to help you track. And its goal stops there. Just write anything in the text box. The more specific you are, the better.&lt;/p&gt;
&lt;p&gt;So here's &lt;a href="https://apps.apple.com/dk/app/taralli/id6743634022"&gt;Taralli&lt;/a&gt;. Food tracking without the BS.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Goodbye Kindle, I don't think I'll miss you.</title><link href="https://duarteocarmo.com/blog/goodbye-kindle-i-dont-think-ill-miss-you.html" rel="alternate"/><published>2025-03-29T00:00:00+01:00</published><updated>2025-03-29T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-03-29:/blog/goodbye-kindle-i-dont-think-ill-miss-you.html</id><summary type="html">&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/78/boox_3.webp" alt="boox palma on table" class="shadow"&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Even though it never &lt;em&gt;really&lt;/em&gt; replaced physical books for me, I've been a &lt;a href="https://kindle-highlights.email/"&gt;big Kindle user&lt;/a&gt; for many years. The ability to take hundreds of books with me anywhere is priceless. Vitto even has a running joke that I tend to lose them on the plane.&lt;/p&gt;
&lt;p&gt;I love my Kindle …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/78/boox_3.webp" alt="boox palma on table" class="shadow"&gt;
&lt;br&gt;&lt;/p&gt;
&lt;p&gt;Even though it never &lt;em&gt;really&lt;/em&gt; replaced physical books for me, I've been a &lt;a href="https://kindle-highlights.email/"&gt;big Kindle user&lt;/a&gt; for many years. The ability to take hundreds of books with me anywhere is priceless. Vitto even has a running joke that I tend to lose them on the plane.&lt;/p&gt;
&lt;p&gt;I love my Kindle so much I &lt;em&gt;never&lt;/em&gt; thought I'd replace it.&lt;/p&gt;
&lt;p&gt;But I did. And it feels GREAT.&lt;/p&gt;
&lt;p&gt;I first heard of the &lt;a href="https://shop.boox.com/products/palma"&gt;Boox Palma&lt;/a&gt; from &lt;a href="https://riccardo.im/"&gt;Riccardo&lt;/a&gt;, and then repeatedly from &lt;a href="https://www.theverge.com/24184777/boox-palma-e-ink-smartphone-reader"&gt;David&lt;/a&gt;. When nerds recommend a new gadget, I'm usually skeptical, but hey, these are &lt;em&gt;respectable&lt;/em&gt; nerds after all. After 2/3 weeks of hesitation, asking Vitto repeatedly if I should go for it (as usual) - I decided to finally press the button.&lt;/p&gt;
&lt;p&gt;From the moment it arrived, I never touched my Kindle ever again.&lt;/p&gt;
&lt;p&gt;There are many good things about the Boox Palma. But I'll start with what hit me first: It's well built. One of my favourite things about the Kindle was that those things would last a long time (if you didn't leave them on the plane). I'm not sure the Boox Palma will last that long - but for the past 4 months of use, I can throw it pretty much everywhere - it's sturdy.&lt;/p&gt;
&lt;p&gt;A Kindle that fits in your pocket. Now how great does that sound? One of my all-time annoyances with the Kindle was the fact that it was great to carry - but ONLY as long as you have a backpack. For airplane travel, it's great. But for every other time I left home without a bag - I just didn't take it. Now, with something that fits in my pocket, every occasion is a good occasion to take my e-reader with me. So I've found myself reading in places I would never have before. Waiting for a friend? Going out for lunch somewhere? Which is one of the big reasons why I love this gadget: it makes me read more!&lt;/p&gt;
&lt;p&gt;We can't talk about an e-reader without talking about the display. I'm by no means a display expert, but it's pretty sharp. You can tweak the refresh modes and filters - but I don't really tend to do so. I do tend to adjust the color temperature. Quite a lot, actually. If I'm reading at night I want something yellower. If I'm reading during the day I want something colder. My only complaint about the display is the lack of a native dark mode. I wish I could touch a button and &lt;em&gt;all&lt;/em&gt; the colors in my device would just invert. But either that option isn't available, or I haven't found it.&lt;/p&gt;
&lt;p&gt;No more Amazon. No more kindle store, no more 'send-to-kindle', no more 'we could not send your file', no more 'when does this thing sync so I can see my book'. Bye Amazon. With the Boox, all I need is Dropbox. The Boox Palma is something the Kindle never was: A full-fledged Android phone. I can install whatever I want, whenever I want. My RSS reader? Check. My newspaper apps? Check. The actual Kindle app? Also!&lt;/p&gt;
&lt;p&gt;But I don't use the Kindle app on my Boox. One of the most pleasant surprises when using the Boox Palma has been KOReader &lt;sup id="sf-goodbye-kindle-i-dont-think-ill-miss-you-1-back"&gt;&lt;a href="#sf-goodbye-kindle-i-dont-think-ill-miss-you-1" class="simple-footnote" title="Kevin Wammer's blog"&gt;1&lt;/a&gt;&lt;/sup&gt;. It's the reading app I've always wanted. And it's where I spend 99% of the time on my Boox Palma. Incredible customization (the fonts, the rendering, the images), dark mode, a super pleasant progress bar. I just love reading in this app. Best of all? It's free and open source. I have to take my hat off to the maintainers. It's another big reason why I love the Boox.&lt;/p&gt;
&lt;p&gt;And it lasts a long time! I tend to read ~1 hour every night, and more during traveling, and it comfortably lasts 2+ weeks without charging. Worth noting that I don't watch YouTube videos on it. You could. But not sure what the point would be.&lt;/p&gt;
&lt;p&gt;The Boox Palma is not perfect. The company has tried to shove AI down the users' throats a couple of times. I think I've received a single software update in ~4 months, which is not the most encouraging. And yes - it's an Android phone, but for everything else other than reading - it will feel sluggish. Expected? Probably. But it also costs 2-3 times what a Kindle does.&lt;/p&gt;
&lt;p&gt;But the reason why I loved my Kindle was not because it was powerful, or because of its great software. It was because it just made me read more often.&lt;/p&gt;
&lt;p&gt;And that's also why I love the Boox Palma. Because it &lt;em&gt;makes&lt;/em&gt; me read more.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Apps I use on the Boox:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://koreader.rocks/"&gt;KOReader&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://getpocket.com/"&gt;Pocket&lt;/a&gt; (not a nice experience - switching to &lt;a href="https://raindrop.io/"&gt;Raindrop&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.economist.com/"&gt;The Economist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.publico.pt/"&gt;Publico&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.seazon.feedme&amp;amp;hl=en&amp;amp;pli=1"&gt;FeedMe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Everything else is in a folder I don't touch&lt;/li&gt;
&lt;/ul&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-goodbye-kindle-i-dont-think-ill-miss-you-1"&gt;Kevin Wammer's &lt;a href="https://cliophate.wtf/"&gt;blog&lt;/a&gt; &lt;a href="#sf-goodbye-kindle-i-dont-think-ill-miss-you-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>You also hate SQL? Let the LLM handle it</title><link href="https://duarteocarmo.com/blog/you-also-hate-sql-let-the-llm-handle-it.html" rel="alternate"/><published>2025-03-09T00:00:00+01:00</published><updated>2025-03-09T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-03-09:/blog/you-also-hate-sql-let-the-llm-handle-it.html</id><summary type="html">&lt;p&gt;Last year I made an effort to speak less and learn more. However, I still had the opportunity to present at a couple of conferences. One of them was &lt;a href="https://www.pyconwroclaw.com/"&gt;PyCon Wroclaw&lt;/a&gt;. The main goal was to talk about a couple of interesting paradigms I've come across while using LLMs to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Last year I made an effort to speak less and learn more. However, I still had the opportunity to present at a couple of conferences. One of them was &lt;a href="https://www.pyconwroclaw.com/"&gt;PyCon Wroclaw&lt;/a&gt;. The main goal was to talk about a couple of interesting paradigms I've come across while using LLMs to help me solve some Text-to-SQL problems.&lt;/p&gt;
&lt;p&gt;Unfortunately the recording came out a bit broken. So I decided to copy a typical post from &lt;a href="https://simonwillison.net/2025/Mar/8/nicar-llms/"&gt;Simon's blog&lt;/a&gt;, and do a small slide walk through here. I won't promise I'll do this for the rest of my &lt;a href="/talks"&gt;talks&lt;/a&gt;. But I think it could be a pretty interesting read.&lt;/p&gt;
&lt;p&gt;I started with my typical introduction:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0001.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0002.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;The goal of the talk was to walk through a 'grab bag' of client cases I've come across recently and five different lessons I've learned.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0004.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;The first chapter was all about setting expectations right. When clients come with a problem nowadays, they always expect a "ChatGPT" style of product.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0005.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0006.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;But ChatGPT isn't a simple product. It's actually a fairly complex piece of software.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0007.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0008.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;Here I introduced what I call the &lt;em&gt;no regrets move&lt;/em&gt;. I.e., what is the minimum thing that you can solve for a certain problem that will always be valuable? In the case of text-to-sql, it's &lt;em&gt;not&lt;/em&gt; the interface. It's just answering questions based on your data.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0009.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;The second chapter was all about getting &lt;em&gt;something out&lt;/em&gt;. Quick. I'm convinced that at least 50% of engineering problems are due to someone developing something in the basement instead of failing fast.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0010.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0011.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;Here I mentioned some tools to get a ChatGPT interface relatively fast. So you can focus on what matters. &lt;a href="https://gist.github.com/duarteocarmo/85435ce9209d1824f68e11b6126ab5c9"&gt;Here's&lt;/a&gt; the code for a ChatGPT-like interface using Streamlit that already includes tools like plotting.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0012.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;This is my favorite advice for any sort of engineering:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0013.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;In the 3rd chapter I finally start focusing on the task at hand. Text-to-SQL.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0014.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;I took some time to present the &lt;a href="https://bird-bench.github.io/"&gt;BIRD-SQL&lt;/a&gt; benchmark. One of the most popular benchmarks for text-to-sql.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0015.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;When looking at the rankings, we can see that the best performing text-to-sql system has an accuracy ~75%. So in your best case scenario. Whatever you build. You can already expect your LLM to get &lt;em&gt;at least&lt;/em&gt; 1/4 questions wrong.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0016.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0017.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;I cover two 'typical' approaches to text-to-sql. And how these approaches can go from something very simple in the right - just by stuffing information into the prompt. To something a bit more &lt;a href="https://arxiv.org/abs/2405.16755"&gt;complex&lt;/a&gt; in the left.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0018.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;But in general. Stuffing information into the prompt is what most people are doing.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0019.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;In the fourth chapter, I go into a good baseline to generate SQL to answer questions about a particular database.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0020.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;I start by talking about the funny 'please give me json' prompts. And show this small bit straight from one of Apple's prompts (Probably Apple Intelligence), where they beg the LLM to return json.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0021.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;But for me, structured outputs are critical for a maintainable and well-built LLM application. So I also take some time to talk about that.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0022.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;I start by introducing &lt;a href="https://python.useinstructor.com/"&gt;instructor&lt;/a&gt;, one of my favorite LLM libraries.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0023.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;Of course, you don't need to use instructor. You can go with &lt;a href="https://platform.openai.com/docs/guides/structured-outputs"&gt;Structured Outputs&lt;/a&gt; as well.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0024.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;But in general - I absolutely hate 'OpenAI code'. E.g., code that is completely locked into OpenAI or any other AI provider. So I present &lt;a href="https://docs.litellm.ai/#litellm-python-sdk"&gt;LiteLLM&lt;/a&gt;. A great tool to keep your code provider agnostic.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0025.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;Here's where &lt;a href="https://docs.litellm.ai/#litellm-python-sdk"&gt;LiteLLM&lt;/a&gt; really shines. When you can switch out &lt;em&gt;any&lt;/em&gt; model from &lt;em&gt;any&lt;/em&gt; provider, just by switching the prompt.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0026.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;As an example I use a simple SQLite database of my running data from Strava.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0027.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;Here's the code to load the Strava data and a small preview of the data.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0029.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;Time to build a basic text-to-sql prompt.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0030.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;My prompt structure. It includes a Preamble, the CREATE TABLE statement so that the LLM knows the structure, some response guidelines, and the user question.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0031.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;The issue with most of these systems is that we have no way to ensure a certain query is actually valid..&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0032.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;Here's a particularly nasty piece of code. I use &lt;a href="https://python.useinstructor.com/"&gt;Instructor&lt;/a&gt; to generate the SQL from a question, but also include a custom validator. This will ensure that whatever the LLM generates can actually be run against the database. This is of course, very dangerous. So one should not put this code in production, since you'll be opening yourself to all sorts of problems. But it's a nice way to ensure &lt;em&gt;validity&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Here's the Frankenstein class:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0033.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;And the function we use to call it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0034.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;Here are some examples of running this system. With two questions. For one, it responds well. For the other, it completely fails the answer. But both answers are smooth and valid from the SQL perspective. But that doesn't mean they are correct.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0035.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;So now that we are getting valid answers, how can we ensure that they are actually good?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0036.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;Once we've started with a baseline. I take Chapter 5 to talk about how to make things better once you have this baseline done.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0037.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;Mandatory meme about testing and performance. If you never test, your tests never fail. If you never evaluate, your LLM system is perfect.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0038.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;To improve your text-to-sql system, you actually need to measure it.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0039.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;This is just like traditional Machine Learning. We can go back to one of the most well-known metrics: Accuracy.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0040.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;To measure accuracy, we can create some questions based on the data from our Database. Use our LLM system to respond to those same questions, and compare both answers. That's it!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0042.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;We can use instructor/structured outputs again here, to understand if our LLM answer matches the information in our baseline answer.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0043.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;I really like looking at data, and evaluating where things go wrong. But I'm skeptical of the myriad of tools coming out to evaluate LLMs. And I'm a big fan of Google Sheets. Mainly because when I look at the data, I probably also look at it with some sort of subject matter expert. So the fewer 'things' between us and the data, the better. And Google Sheets are fine.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0044.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;Here's an example of how I use Google Sheets to look at the performance of this text-to-sql system. You can see the question we want answered. The SQL and answer from our ground truth, the SQL and answer from the LLM system, and its evaluation. We also include a reason why a certain case was FAIL/PARTIAL.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0045.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;This is all nice and dandy. But we should never forget the big picture. So what?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0046.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;In the final slide, I come back to the three most important lessons: Iterate quickly, make the SQL generation robust and explainable, and iterate from a baseline.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/77/pycon-wroclaw-llm-text-to-sql_page-0047.webp" class="shadow"/&gt;&lt;/p&gt;
&lt;p&gt;I then took some questions and wrapped things up!&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Simple AI tools</title><link href="https://duarteocarmo.com/blog/simple-ai-tools.html" rel="alternate"/><published>2025-02-18T00:00:00+01:00</published><updated>2025-02-18T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-02-18:/blog/simple-ai-tools.html</id><summary type="html">&lt;p&gt;Last week over lunch, &lt;a href="https://www.parraguezr.net/"&gt;Pedro&lt;/a&gt; was telling me how he uses &lt;a href="https://www.perplexity.ai/"&gt;Perplexity&lt;/a&gt; to improve the way he searches around the web. He mentioned we're entering a phase where we have at least 20 'AI' tools to boost our productivity - and the tough thing now is choosing which ones to use …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Last week over lunch, &lt;a href="https://www.parraguezr.net/"&gt;Pedro&lt;/a&gt; was telling me how he uses &lt;a href="https://www.perplexity.ai/"&gt;Perplexity&lt;/a&gt; to improve the way he searches around the web. He mentioned we're entering a phase where we have at least 20 'AI' tools to boost our productivity - and the tough thing now is choosing which ones to use.&lt;/p&gt;
&lt;p&gt;With so much buzz around AI - it's easy to feel overwhelmed and think "I don't use that, maybe I should?". Now - I don't know which tools are the best for your particular use case. But for the past 6-10 months, I've settled on a nice group of tools I think more people would benefit from.&lt;/p&gt;
&lt;h2 id="for-everything-else"&gt;For everything else&lt;/h2&gt;
&lt;p&gt;I recently upgraded my MacBook to an M3 Max with 64GB of RAM. One of the main reasons I got it was so that I could run a nice suite of Large Language Models (LLMs) locally. &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt; has been at the center of that workflow - (&lt;a href="https://www.reddit.com/r/ollama/comments/1idqxto/comment/ma19shz/?utm_source=share&amp;amp;utm_medium=web3x&amp;amp;utm_name=web3xcss&amp;amp;utm_term=1&amp;amp;utm_content=share_button"&gt;don't forget&lt;/a&gt;). Whenever a new &lt;a href="https://ollama.com/library/deepseek-r1"&gt;model&lt;/a&gt; comes out, it's just an &lt;code&gt;ollama pull&lt;/code&gt; away - and you're up and running.&lt;/p&gt;
&lt;p&gt;Models are great - but just like ChatGPT showed everyone, the interface really makes the difference. Since I'm a Mac user, I wanted a unified way to interact with all the models I'm interested in - in a nice interface. &lt;a href="https://boltai.com?aff=2OOJDR"&gt;BoltAI&lt;/a&gt; does just that.&lt;/p&gt;
&lt;p&gt;There are a lot of things I like about BoltAI. But my favorite one is that I can use a single interface for just about any model under the sun (and running on my laptop). I've also setup &lt;a href="https://openrouter.ai/"&gt;OpenRouter&lt;/a&gt;, so I can try out any tropical model/provider combo with just a few clicks. It's a one time purchase - and &lt;a href="https://danielnguyen.me/"&gt;Daniel&lt;/a&gt; is &lt;a href="https://boltai.canny.io/"&gt;super responsive&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="BoltAI LLM application" src="https://duarteocarmo.com/images/76/boltai.webp" /&gt;&lt;/p&gt;
&lt;p&gt;There are two things that are currently missing from my workflow for everyday tools and I haven't solved yet. The first is some sort of canvas interface, &lt;a href="https://support.anthropic.com/en/articles/9487310-what-are-artifacts-and-how-do-i-use-them"&gt;artifacts&lt;/a&gt; are my favorite for this. The second thing I'm missing is some sort of LLM + web search integration. I've been experimenting with &lt;a href="https://www.perplexity.ai/"&gt;Perplexity&lt;/a&gt;, &lt;a href="https://www.phind.com/"&gt;Phind&lt;/a&gt;, and some &lt;a href="https://docs.boltai.com/docs/chat-ui/ai-plugins"&gt;plugins&lt;/a&gt; for BoltAI - but I'm still skeptical of LLM's interpretation of search results.&lt;/p&gt;
&lt;p&gt;Great, what about engineering?&lt;/p&gt;
&lt;h2 id="for-code"&gt;For code&lt;/h2&gt;
&lt;p&gt;My first purchase of any AI service at all was (and still is) GitHub's &lt;a href="https://github.com/features/copilot"&gt;copilot&lt;/a&gt;. I've tried everything under the sun - &lt;a href="https://codeium.com/"&gt;Codeium&lt;/a&gt;, &lt;a href="https://codeium.com/"&gt;SuperMaven&lt;/a&gt;, but none comes quite close to GitHub. I consider it a commodity now, just like my LSP's completion.&lt;/p&gt;
&lt;p&gt;I'm still, largely a &lt;a href="https://github.com/duarteocarmo/dotfiles/tree/master/.config/nvim"&gt;NeoVim&lt;/a&gt; user. And the one of the most useful LLM tools/plugins I've used with it is &lt;a href="https://github.com/Robitx/gp.nvim"&gt;Gp.nvim&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It's an amazing plugin. It allows me to generate new code, re-write portions of the code base, or even create a floating chat window to chat with a particular part of the codebase I want to understand better. Oh, and it also support Ollama models - so that the magic doesn't stop - even when I'm on an airplane.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Gif of GpNvim in action" src="https://duarteocarmo.com/images/76/gpnvim.gif" /&gt;&lt;/p&gt;
&lt;p&gt;Even if I like NeoVim, I'll admit that for certain projects - it doesn't quite cut it. For larger codebases, using notebooks, or when I &lt;em&gt;feel like it&lt;/em&gt;, &lt;a href="https://www.cursor.com/"&gt;Cursor&lt;/a&gt; has also been a booster. Particularly when you're creating something new, rather tweaking something that exists already. A small static page - some css styling on a template - it can handle most of that.&lt;/p&gt;
&lt;p&gt;A tool I've been exploring more lately is &lt;a href="https://aider.chat/"&gt;Aider&lt;/a&gt;. Think of it as Cursor, but terminal based - with a git integration. Aider gives me a bit more control over the changes I want it to make. It allows me to only select certain files where it operates, it can also run commands - and examine their output. It's a super nice tool - especially if you prefer a terminal + git workflow. As an added bonus, Aider also supports &lt;a href="https://aider.chat/docs/llms/ollama.html"&gt;local&lt;/a&gt; models. To be completely honest - I think I've barely even &lt;a href="https://aider.chat/docs/usage/commands.html"&gt;scratched&lt;/a&gt; what's possible with Aider.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Aider LLM tool in action" src="https://duarteocarmo.com/images/76/aider.gif" /&gt;&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;On tools.&lt;/strong&gt; I agree with Pedro - there's no shortage of tools out there. Everyone will tell you to use X, use Y, or charge you 20 USD for it. Like most things, tools are only as good as you're productive with them - and very person specific. The best tool for me is probably not the best tool for you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exploration is key.&lt;/strong&gt; I believe one of the most important things is to keep exploring what's out there. Don't dismiss something just because it's new. A great new &lt;a href="https://www.deepseek.com/"&gt;model&lt;/a&gt;? Take it for a spin.. A transformative &lt;a href="https://www.all-hands.dev/"&gt;IDE&lt;/a&gt;? Give it a shot and see what it's capable of. You might just find something you like - and &lt;em&gt;a lot&lt;/em&gt; of things you don't.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Local first.&lt;/strong&gt; If there's something we learned in the last few months, is that we probably don't need so much compute like we thought we did. Local models are getting really good, really fast. You might have noticed that almost all tools shared here also support local models that I'm running on my own machine? Are they as good as the big ones? No. Well, at least &lt;em&gt;not yet&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Keep exploring.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>What the hell Is GPQA, anyway?</title><link href="https://duarteocarmo.com/blog/what-the-hell-is-gqpa-anyway.html" rel="alternate"/><published>2025-01-15T00:00:00+01:00</published><updated>2025-01-15T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2025-01-15:/blog/what-the-hell-is-gqpa-anyway.html</id><summary type="html">&lt;p&gt;In the period of the generative AI boom, everything is happening all at once. Every day a new model comes out, every week a billion dollar company teases a great new advancement.&lt;/p&gt;
&lt;p&gt;More often than not, they'll show us &lt;a href="https://www.anthropic.com/news/claude-3-family"&gt;some&lt;/a&gt; &lt;a href="https://ai.meta.com/blog/llama-3-2-connect-2024-vision-edge-mobile-devices/"&gt;fancy&lt;/a&gt; &lt;a href="https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/"&gt;version&lt;/a&gt; of this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/75/0c345aa068fce110d80e914f8cd8898d.png" alt="Benchmark example" style="max-width:100%;border-radius: 2px;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.theverge.com/2024/10/28/24281965/honestly-this-is-a-work-of-art"&gt;We all love a misterious chart …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;In the period of the generative AI boom, everything is happening all at once. Every day a new model comes out, every week a billion dollar company teases a great new advancement.&lt;/p&gt;
&lt;p&gt;More often than not, they'll show us &lt;a href="https://www.anthropic.com/news/claude-3-family"&gt;some&lt;/a&gt; &lt;a href="https://ai.meta.com/blog/llama-3-2-connect-2024-vision-edge-mobile-devices/"&gt;fancy&lt;/a&gt; &lt;a href="https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/"&gt;version&lt;/a&gt; of this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/75/0c345aa068fce110d80e914f8cd8898d.png" alt="Benchmark example" style="max-width:100%;border-radius: 2px;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.theverge.com/2024/10/28/24281965/honestly-this-is-a-work-of-art"&gt;We all love a misterious chart.&lt;/a&gt;  &lt;sup id="sf-what-the-hell-is-gqpa-anyway-1-back"&gt;&lt;a href="#sf-what-the-hell-is-gqpa-anyway-1" class="simple-footnote" title="Source"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;A healthy person might look at this and just think - great - the higher the score the better the model. Unfortunately - I'm &lt;a href="https://duarteocarmo.com/blog/the-marathon"&gt;not healthy&lt;/a&gt;, and I'm a bit sick we just throw these tables around like they're the absolute truth.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;40.9 score in GPQA&lt;/strong&gt; - what the &lt;em&gt;hell&lt;/em&gt; does that even mean?&lt;/p&gt;
&lt;h2 id="code"&gt;Code&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/duarteocarmo/3eeb2200af74880e034d45e63e30aafe"&gt;Code and prompts&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="a-quick-intro-to-gpqa"&gt;A quick intro to GPQA&lt;/h2&gt;
&lt;p&gt;GPQA stands for "Graduate-Level Google-Proof Q\&amp;amp;A Benchmark." It is a &lt;a href="https://huggingface.co/datasets/Idavidrein/gpqa"&gt;dataset&lt;/a&gt; and &lt;a href="https://github.com/idavidrein/gpqa"&gt;methodology&lt;/a&gt; introduced in a &lt;a href="https://arxiv.org/pdf/2311.12022"&gt;paper&lt;/a&gt; by researchers from NYU, Cohere, and Anthropic. In essence, GPQA focuses on a subset of exceptionally challenging questions from the fields of physics, chemistry, and biology.&lt;/p&gt;
&lt;p&gt;The dataset is available in three versions, each with varying levels of difficulty:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Extended Set&lt;/strong&gt;: 546 questions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Main Set&lt;/strong&gt;: 448 questions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diamond Set&lt;/strong&gt;: 198 questions (the most challenging subset).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's an example of a question from the dataset:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Methylcyclopentadiene (which exists as a fluxional mixture of isomers) was allowed to react with methyl isoamyl ketone
and a catalytic amount of pyrrolidine. A bright yellow, cross-conjugated polyalkenyl hydrocarbon product formed (as
a mixture of isomers), with water as a side product. These products are derivatives of fulvene. This product was then
allowed to react with ethyl acrylate in a 1:1 ratio. Upon completion of the reaction, the bright yellow color had disappeared.
How many chemically distinct isomers make up the final product (not counting stereoisomers)?
A) 2
B) 16
C) 8
D) 4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For context, a 'non-expert' human-with unlimited time and internet access-achieves roughly 30.4% accuracy on the main set.&lt;/p&gt;
&lt;p&gt;With that in mind, it's intriguing to see how large language models (LLMs) are tested against these tasks. Fortunately, the prompts used for evaluation are openly shared in both the &lt;a href="https://arxiv.org/pdf/2311.12022"&gt;paper&lt;/a&gt; and the &lt;a href="https://github.com/idavidrein/gpqa"&gt;repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Zero-shot prompt:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;What is the correct answer to this question: What is the solubility of the stochiometric tricalcium
phosphate in a solution with the pH level of a) pH = 9, b) pH = 7 and c) pH = 5 (25 °C)?
The Ksp for Ca3(PO4)2 is 2𝑥10- 29, the dissociation constants of phosphoric acid are Ka1 =
7.5𝑥10- 3, Ka2 = 6.2𝑥10- 8 and Ka3 = 1.8𝑥10- 12.
Choices:
(A) 1.96𝑥10- 5; 1.35𝑥10- 4; 4.37𝑥10- 3
(B) 1.67𝑥10- 5; 1.51𝑥10- 4; 4.81𝑥10- 3
(C) 1.54𝑥10- 5𝑀; 1.42𝑥10- 4; 4.67𝑥10- 3
(D) 1.43𝑥10- 5; 1.29𝑥10- 4; 4.58𝑥10- 3

Format your response as follows: "The correct answer is (insert answer here)".
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Zero-shot chain-of-thought prompt:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;What is the correct answer to this question: What is the solubility of the stochiometric tricalcium
phosphate in a solution with the pH level of a) pH = 9, b) pH = 7 and c) pH = 5 (25 °C)?
The Ksp for Ca3(PO4)2 is 2𝑥10- 29, the dissociation constants of phosphoric acid are Ka1 =
7.5𝑥10- 3, Ka2 = 6.2𝑥10- 8 and Ka3 = 1.8𝑥10- 12.
Choices:
(A) 1.96𝑥10- 5; 1.35𝑥10- 4; 4.37𝑥10- 3
(B) 1.67𝑥10- 5; 1.51𝑥10- 4; 4.81𝑥10- 3
(C) 1.54𝑥10- 5𝑀; 1.42𝑥10- 4; 4.67𝑥10- 3
(D) 1.43𝑥10- 5; 1.29𝑥10- 4; 4.58𝑥10- 3
Let's think step by step:
...MODEL RESPONSE HERE...

Based on the above, what is the single, most likely answer choice? Answer in the format "The
correct answer is (insert answer here)".
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A curious reader might notice that the 'zero-shot' chain-of-thought (COT) prompt actually involves two calls to the model: the first to generate a response and the second to finalize the answer. This approach is likely &lt;strong&gt;not&lt;/strong&gt; representative of how these models are typically used in real-world scenarios-but we'll explore that further in a moment.&lt;/p&gt;
&lt;p&gt;This raises an interesting question: how can we reproduce these results ourselves?&lt;/p&gt;
&lt;h2 id="trying-to-reproduce-gpqa"&gt;Trying to reproduce GPQA&lt;/h2&gt;
&lt;p&gt;If you've explored LLM evaluations, you're likely familiar with the &lt;a href="https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard#/?params=-1%2C8\&amp;amp;official=true\&amp;amp;rankingMode=dynamic\&amp;amp;columns=rank%2Cmodel.type_icon%2Cid%2Cmodel.average_score%2Cmetadata.params_billions%2Cmetadata.submission_date%2Cevaluations.gpqa.normalized_score%2Cmetadata.base_model%2Cevaluations.mmlu_pro.normalized_score%2Cevaluations.musr.normalized_score"&gt;Open LLM Leaderboard&lt;/a&gt; by Hugging Face. The leaderboard recently received an update, allowing users to filter by 'Only Official Providers'-handy if you want to focus on well-known providers.&lt;/p&gt;
&lt;p&gt;Here's a snapshot of the top-performing models under 8 billion parameters, sorted by GPQA score:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/75/86a23c7296cb79b43c2eff265f26cf56.png" alt="Open LLM leaderboard" style="max-width:100%;border-radius: 2px;"&gt;&lt;/p&gt;
&lt;p&gt;You'll notice there are some familiar names in there like Phi, or Qwen 2.5 7B. Of course, models from OpenAI, Google or Claude are off this list, since it relates to Open models - which those aren't.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://huggingface.co/docs/leaderboards/open_llm_leaderboard/about"&gt;Digging a bit deeper&lt;/a&gt;, we notice two interesting things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;This leaderboad uses the zero-shot version of this dataset, and measures the normalized accuracy. It doesn't specify which variant of the dataset it uses - we'll assume the &lt;code&gt;main&lt;/code&gt; variant for now - but that could be wrong.&lt;/li&gt;
&lt;li&gt;Another intesting tid-bit is how they run these benchmarks: '&lt;em&gt;...using the &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness"&gt;Eleuther AI Language Model Evaluation Harness&lt;/a&gt; , a unified framework to test generative language models...&lt;/em&gt;'&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Great - so now we have at least &lt;em&gt;some&lt;/em&gt; clue of how to reproduce this.&lt;/p&gt;
&lt;h2 id="testing-eleutherais-lm-evaluation-harness"&gt;Testing EleutherAI's LM Evaluation Harness&lt;/h2&gt;
&lt;p&gt;There are &lt;a href="https://github.com/openai/simple-evals?tab=readme-ov-file"&gt;several frameworks&lt;/a&gt; for evaluating large language models (LLMs), including &lt;a href="https://github.com/openai/evals"&gt;OpenAI's evals&lt;/a&gt; and EleutherAI's &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness"&gt;LM Evaluation Harness&lt;/a&gt;. EleutherAI's framework, while powerful, can feel overwhelming at first. It supports a wide range of &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness?tab=readme-ov-file#model-apis-and-inference-servers"&gt;models and APIs&lt;/a&gt; and a significant number of &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness/tree/main/lm_eval/tasks#tasks"&gt;tasks&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;After some poking around, I managed to find the &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness/blob/main/lm_eval/tasks/gpqa/README.md#tasks"&gt;part of the repo&lt;/a&gt; that relates the benchmark we're interested in.&lt;/p&gt;
&lt;p&gt;We can see that they support the following &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness/tree/main/lm_eval/tasks/gpqa#tasks"&gt;tasks&lt;/a&gt;:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;* `gpqa_{main, diamond, extended}_zeroshot`
* `gpqa_{main, diamond, extended}_n_shot`
* `gpqa_{main, diamond, extended}_generative_n_shot`
* `gpqa_{main, diamond, extended}_cot_zeroshot`
* `gpqa_{main, diamond, extended}_cot_n_shot`
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can also see the &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness/blob/main/lm_eval/tasks/gpqa/zeroshot/_gpqa_zeroshot_yaml#L9"&gt;config and prompt&lt;/a&gt; they use for the &lt;code&gt;gpqa_main_zeroshot&lt;/code&gt; task which is the one we're interested in. The attentive reader might notice already that &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness/blob/bb098f13b05e361f01a5afe7b612779ce362b3f2/lm_eval/tasks/gpqa/zeroshot/_gpqa_zeroshot_yaml#L9"&gt;the prompt&lt;/a&gt; that is being used here is quite different from the one from the original paper. But let's ignore that for now.&lt;/p&gt;
&lt;p&gt;Here's how I tried to run the evaluation:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;lm-eval --model openai-chat-completions \
        --model_args model=gpt-4o-mini,num_concurrent=10,max_retries=3 \
        --tasks gpqa_main_zeroshot \
        --apply_chat_template \
        --output_path results \
        --log_samples \
        --wandb_args project=lmeval \
        --use_cache model_cache
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This errors out:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;NotImplementedError: Loglikelihood (and therefore `multiple_choice`-type tasks) is not supported for chat completions as OpenAI does not provide prompt logprobs. See https://github.com/EleutherAI/lm-evaluation-harness/issues/942#issuecomment-1777836312 or https://github.com/EleutherAI/lm-evaluation-harness/issues/1196 for more background on this limitation.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Oh wait? What's happening here? Well - most of these multiple-choice benchmarks use loglikelihood to understand what the choice is with the highest probability. This is a bit weird - since it's not the methodology outlined in the original GPQA paper. However, seems to be a deliberate choice to maintain the framework consistent.&lt;sup id="sf-what-the-hell-is-gqpa-anyway-2-back"&gt;&lt;a href="#sf-what-the-hell-is-gqpa-anyway-2" class="simple-footnote" title="See this and this."&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;However, there's another benchmark we could run and appears to work - the zero-shot chain-of-thought one.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;lm-eval --model openai-chat-completions \
        --model_args model=gpt-4o-mini,num_concurrent=10,max_retries=3 \
        --tasks gpqa_main_cot_zeroshot \ #&amp;lt;- cot version
        --apply_chat_template \
        --output_path results \
        --log_samples \
        --wandb_args project=lmeval \
        --use_cache model_cache
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gives us:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;|        Tasks         |Version|     Filter     |n-shot|  Metric   |   |Value |   |Stderr|
|----------------------|------:|----------------|-----:|-----------|---|-----:|---|-----:|
|gpqa_main_cot_zeroshot|      1|flexible-extract|     0|exact_match|↑  |0.1205|±  |0.0154|
|                      |       |strict-match    |     0|exact_match|↑  |0.0000|±  |0.0000|
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, I haven't dug too deeply into these results for the COT version, but they seem very low since they rely on the &lt;code&gt;flexible-extract&lt;/code&gt; metric, which &lt;em&gt;could&lt;/em&gt; perform poorly if it's regex-based. One thing we notice is that the &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness/blob/main/lm_eval/tasks/gpqa/cot_zeroshot/_gpqa_cot_zeroshot_yaml#L9"&gt;prompt&lt;/a&gt; used here is closer to the original one from the paper. However, it lacks the two-model-call approach outlined there.&lt;/p&gt;
&lt;p&gt;At this point, I was starting to lose faith in this entire evaluation system. What's the most straightforward way to achieve an interesting score on this benchmark? Something that can work for any model, whether open or closed?&lt;/p&gt;
&lt;h2 id="running-gpqa-zero-shot-with-dspy"&gt;Running GPQA zero-shot with DsPy&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://dspy.ai/"&gt;DSPy&lt;/a&gt; is a great framework out of Standford that provides some structure around prompting for LLMs. It also provides some great optimizers so we can programatically optimize a prompt without having to write much of it.&lt;/p&gt;
&lt;p&gt;We start by getting the data in:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;polars&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pl&lt;/span&gt;

&lt;span class="c1"&gt;# Load the dataset from Hugging Face and prepare it&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scan_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hf://datasets/Idavidrein/gpqa/*_main.csv"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Question"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The next step is to configure our llm and the Signatures (think data-model) we'll use:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;lm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"openai/gpt-4o-mini"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.00&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Signature&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Respond to the question with the correct answer"""&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;generate_answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here's an example of how to use it:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate_answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"What's the capital of Le Marche?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Ancona"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Senigallia"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# prints Prediction(answer='Ancona')&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's juggle our data into a dataset:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_dicts&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;choices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Correct Answer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Incorrect Answer 1"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Incorrect Answer 2"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Incorrect Answer 3"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;example&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Question"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;correct_answer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Correct Answer"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_inputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"choices"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;EvaluateZeroShot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Signature&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Evaluate the answer to the question"""&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;correct_answer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;is_correct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;evaluate_answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EvaluateZeroShot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;validate_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evaluate_answer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;correct_answer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;correct_answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_correct&lt;/span&gt;

&lt;span class="n"&gt;evaluator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;devset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;trainset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;display_progress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;display_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;accuracy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evaluator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;generate_answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;validate_response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Accuracy: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;accuracy&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# prints 33.48&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Great, so a single run for GPQA gives us a 33.48% accuracy. Still a bit far off the 40.2% that is &lt;a href="https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/"&gt;advertised by OpenAI&lt;/a&gt; - but again - there's apparently no way we can effectively verify that.&lt;/p&gt;
&lt;h2 id="a-small-dspy-side-track"&gt;A small DSPy side-track&lt;/h2&gt;
&lt;p&gt;There's a couple of interesting experiments we run with DSPY.&lt;/p&gt;
&lt;p&gt;The first one, is to try a Chain-of-thought prompt (COT) to understand how that might influence our score:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;cot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChainOfThought&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"question, choices -&amp;gt; answer"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;accuracy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evaluator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;validate_response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Accuracy: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;accuracy&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# prints 35.71&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That's a ~7% increase on our GPQA score. Probably just noise.&lt;/p&gt;
&lt;p&gt;Another interesting thing we could do, is to try to optimize our prompt for this specific GPQA benchmark. An important note is that we don't need to even fine-tune a model, we're just going to optimize our prompt.&lt;/p&gt;
&lt;p&gt;To do that, we'll use 98 questions that are in the extended version of the dataset, but not in the main version of it. Our goal is to optimize on those 98 and then see if an optimized prompt performs better on our main version.&lt;/p&gt;
&lt;p&gt;Here we go:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;df_extended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scan_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"hf://datasets/Idavidrein/gpqa/*_extended.csv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Question"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df_extended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df_extended&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Question"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df_main&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Question"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="c1"&gt;# returns 98 questions&lt;/span&gt;

&lt;span class="c1"&gt;# build extended_set from df_extended (omitted for brevity)&lt;/span&gt;

&lt;span class="n"&gt;cot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ChainOfThought&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"question, choices -&amp;gt; answer"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# base format&lt;/span&gt;
&lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MIPROv2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;validate_response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auto&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"light"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# optimization&lt;/span&gt;
&lt;span class="n"&gt;optimized_cot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trainset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;extended_set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# run optimization&lt;/span&gt;

&lt;span class="c1"&gt;# evaluate optimized prompt&lt;/span&gt;

&lt;span class="n"&gt;accuracy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evaluator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;optimized_cot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;validate_response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Accuracy: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;accuracy&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# prints 38.39&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This produces an &lt;a href="https://gist.github.com/duarteocarmo/3eeb2200af74880e034d45e63e30aafe#file-optimized_cot-json"&gt;optimized prompt&lt;/a&gt; with some few example questions. When evaluating, we get a further increased score of another 7.5%. As you can see - the results of these benchmark are HIGHLY prompt dependent. It seems to be possible to even optimize a much smaller model to be able to perform well on this 'benchmark'.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;Alright, after this deep dive, you should have a clearer idea of what GPQA is and how it works. Most benchmarks you see out there are variations of these multi-choice questions-some harder, some easier, some slightly different.&lt;/p&gt;
&lt;p&gt;But this exercise also revealed a few important things about the benchmark ecosystem and its flaws:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benchmarks are not scientific.&lt;/strong&gt;
Many benchmarks feel more like pseudo-science than systematic evaluations. Frameworks often fail to replicate the original paper's prompts and methodology. If this inconsistency exists here, imagine how it plays out in other benchmarks. Using log-likelihood to evaluate multi-choice questions might make sense in some cases, but the GPQA paper doesn't mention it at all. So, what's really going on here?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prompts matter-a lot.&lt;/strong&gt;
As we saw with DSPy, small tweaks in prompts can make a huge difference, sometimes boosting scores by 5-10%. Add a "you are a physics expert" to your prompt, and you might get even better results. This raises the question: how sensitive are these benchmarks to prompts? Are some models more affected than others? Definitely worth exploring.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The issue of trust.&lt;/strong&gt;
Benchmarks should inspire confidence in a model's performance, but instead, they often raise more questions than answers. For instance, what &lt;a href="https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/"&gt;does a 40.2 GPQA score for GPT-4o-mini really mean&lt;/a&gt;? What dataset version? What prompt? Without these details, it's hard to trust the results-especially when they're not reproducible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How can we evaluate?&lt;/strong&gt;
I once heard in a podcast: "There's nothing like a good vibe evaluation." Sure, when building products with LLMs, good &lt;a href="https://hamel.dev/blog/posts/evals/"&gt;evaluations&lt;/a&gt; matter. But when comparing models, questions of &lt;a href="https://lmarena.ai/"&gt;preference&lt;/a&gt; are often more valuable than multi-choice benchmarks. Efforts like the &lt;a href="https://huggingface.co/spaces/open-llm-leaderboard/open_llm_leaderboard"&gt;OpenLLM leaderboard&lt;/a&gt; are interesting, but do they really tell us much about these models? With closed models, at least, the mystery adds intrigue.&lt;/p&gt;
&lt;p&gt;Benchmarks are fun, but in the end, the true test of a model is how well it performs in the real world. That's what matters.&lt;/p&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-what-the-hell-is-gqpa-anyway-1"&gt;&lt;a href="https://arxiv.org/pdf/2412.08905"&gt;Source&lt;/a&gt; &lt;a href="#sf-what-the-hell-is-gqpa-anyway-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-what-the-hell-is-gqpa-anyway-2"&gt;See &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness/issues/942#issuecomment-1777836312"&gt;this&lt;/a&gt; and &lt;a href="https://github.com/EleutherAI/lm-evaluation-harness/issues/1196"&gt;this&lt;/a&gt;. &lt;a href="#sf-what-the-hell-is-gqpa-anyway-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>RAG tricks from the trenches</title><link href="https://duarteocarmo.com/blog/rag-tricks-from-the-trenches.html" rel="alternate"/><published>2024-12-30T00:00:00+01:00</published><updated>2024-12-30T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-12-30:/blog/rag-tricks-from-the-trenches.html</id><summary type="html">&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/74/main.jpg" alt="Men fishing" style="max-width:100%;border-radius: 2px;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/duarteocarmo/79346dacdfcccbf0ed9184ddbaa49a32"&gt;Notebook download&lt;/a&gt; - &lt;a href="http://nbsanity.com/static/1637e83c083a69e81cfa19006502fced/summarization.html"&gt;nbsanity preview&lt;/a&gt;&lt;/p&gt;
&lt;!-- https://www.artic.edu/artworks/16837/tynemouth-priory-england --&gt;

&lt;h2 id="some-context"&gt;Some context&lt;/h2&gt;
&lt;p&gt;We had a database of 50M strings and I couldn't wait to embed them all. Embeddings for recommender systems were, &lt;em&gt;for a long time&lt;/em&gt;, the HOLY grail I longed for. I still remember, long before the RAG rage, having conversations with &lt;a href="https://www.parraguezr.net/"&gt;Pedro&lt;/a&gt; about the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/74/main.jpg" alt="Men fishing" style="max-width:100%;border-radius: 2px;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/duarteocarmo/79346dacdfcccbf0ed9184ddbaa49a32"&gt;Notebook download&lt;/a&gt; - &lt;a href="http://nbsanity.com/static/1637e83c083a69e81cfa19006502fced/summarization.html"&gt;nbsanity preview&lt;/a&gt;&lt;/p&gt;
&lt;!-- https://www.artic.edu/artworks/16837/tynemouth-priory-england --&gt;

&lt;h2 id="some-context"&gt;Some context&lt;/h2&gt;
&lt;p&gt;We had a database of 50M strings and I couldn't wait to embed them all. Embeddings for recommender systems were, &lt;em&gt;for a long time&lt;/em&gt;, the HOLY grail I longed for. I still remember, long before the RAG rage, having conversations with &lt;a href="https://www.parraguezr.net/"&gt;Pedro&lt;/a&gt; about the amazing types of applications we could build with embeddings.&lt;/p&gt;
&lt;p&gt;In a way - RAG has come a &lt;strong&gt;long&lt;/strong&gt; way. From another side: but RAG is still &lt;em&gt;the same&lt;/em&gt;. It's just search! Over embeddings.&lt;/p&gt;
&lt;p&gt;For the past 2/3 years, the number of applications doing some sort of RAG has increased significantly. I've learned a trick or two over that time.&lt;/p&gt;
&lt;p&gt;Here are some tricks I've learned along the way in hope someone else can benefit from them as well.&lt;/p&gt;
&lt;p&gt;Important notice: Almost &lt;em&gt;none&lt;/em&gt; of these are mine. And that's why I like them.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# !pip install polars jupyter_black fasttext huggingface_hub lancedb litellm sklearn sentence_transformers&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;polars&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pl&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;jupyter_black&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;fasttext&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;huggingface_hub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hf_hub_download&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;lancedb&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;lancedb.pydantic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LanceModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Vector&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;lancedb.embeddings&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_registry&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;litellm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sklearn.cluster&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KMeans&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_random_seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;jupyter_black&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# CONFIGURATION&lt;/span&gt;

&lt;span class="n"&gt;EMBEDDING_MODEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;all-MiniLM-L6-v2&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;LLM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;ollama_chat/llama3.1&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;skeets&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;DEVICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;mps&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="our-data-a-controversial-dataset"&gt;Our data: A controversial dataset&lt;/h2&gt;
&lt;p&gt;Remember that &lt;a href="https://www.404media.co/someone-made-a-dataset-of-one-million-bluesky-posts-for-machine-learning-research/"&gt;guy that got pretty much banned from BlueSky&lt;/a&gt; for collecting a dataset of 1 Million skeets? Well, our dataset is a nice collection of not 1, but 2! &lt;a href="https://huggingface.co/datasets/alpindale/two-million-bluesky-posts"&gt;2 Million BlueSky posts&lt;/a&gt;. Isn't that controversial?&lt;/p&gt;
&lt;p&gt;I'm really enjoying Bluesky by the way. You should &lt;a href="https://bsky.app/profile/duarteocarmo.com"&gt;follow me&lt;/a&gt; there in case you come across this!&lt;/p&gt;
&lt;p&gt;Alright, let's load the dataset using Polars:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scan_ndjson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;hf://datasets/alpindale/two-million-bluesky-posts/*.jsonl&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;reply_to&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_null&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;created_at&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Datetime&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;created_at&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%Y-%m&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;month_year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;created_at&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;%Y&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# so that we run fast.&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="si"&gt;=}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Example skeets:&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_dicts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;df.shape=(50000, 8)
Example skeets:
[{&amp;#39;text&amp;#39;: &amp;quot;The last time I was back in the U.S. to visit family for Thanksgiving was in 2008, shortly after Obama was elected. People were elated and optimistic. The future looked bright.\nToday it could hardly be darker. That&amp;#39;s what the MAGAs have brought us: chaotic dictatorship.\nI&amp;#39;ll stay home in Canada.&amp;quot;,
  &amp;#39;created_at&amp;#39;: datetime.datetime(2024, 11, 27, 15, 30, 22, 133000),
  &amp;#39;author&amp;#39;: &amp;#39;did:plc:cnxppns7qxo2wzdlutmxbkih&amp;#39;,
  &amp;#39;uri&amp;#39;: &amp;#39;at://did:plc:cnxppns7qxo2wzdlutmxbkih/app.bsky.feed.post/3lbwumhstsc2e&amp;#39;,
  &amp;#39;has_images&amp;#39;: False,
  &amp;#39;reply_to&amp;#39;: None,
  &amp;#39;month_year&amp;#39;: &amp;#39;2024-11&amp;#39;,
  &amp;#39;year&amp;#39;: &amp;#39;2024&amp;#39;},
 {&amp;#39;text&amp;#39;: &amp;#39;9 Top CSS Essential Skills That Every Web designer Should Learn – http://bit.ly/vVyTg #CSS #webdesign&amp;#39;,
  &amp;#39;created_at&amp;#39;: datetime.datetime(2009, 10, 17, 17, 32, 37),
  &amp;#39;author&amp;#39;: &amp;#39;did:plc:id2rbt2kysdnscdrzh5o5n7d&amp;#39;,
  &amp;#39;uri&amp;#39;: &amp;#39;at://did:plc:id2rbt2kysdnscdrzh5o5n7d/app.bsky.feed.post/3lbwmyaeat42n&amp;#39;,
  &amp;#39;has_images&amp;#39;: False,
  &amp;#39;reply_to&amp;#39;: None,
  &amp;#39;month_year&amp;#39;: &amp;#39;2009-10&amp;#39;,
  &amp;#39;year&amp;#39;: &amp;#39;2009&amp;#39;}]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Without much surprise, most of our posts (88%) are from November '24.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;min_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;created_at&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;max_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;created_at&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;total_months&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;min_date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;year&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;min_date&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;min_date: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;min_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, max_date: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;total_months: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_months&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;month_year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value_counts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;month_year&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;descending&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;min_date: 2007-10-19 21:50:33, max_date: 2024-11-28 05:08:13.636000
total_months: 205
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;month_year&lt;/th&gt;
&lt;th&gt;proportion&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2024-11&lt;/td&gt;
&lt;td&gt;0.87644&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-10&lt;/td&gt;
&lt;td&gt;0.00028&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-09&lt;/td&gt;
&lt;td&gt;0.00028&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-08&lt;/td&gt;
&lt;td&gt;0.00038&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-07&lt;/td&gt;
&lt;td&gt;0.00072&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-06&lt;/td&gt;
&lt;td&gt;0.00032&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-05&lt;/td&gt;
&lt;td&gt;0.00024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-04&lt;/td&gt;
&lt;td&gt;0.00036&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-03&lt;/td&gt;
&lt;td&gt;0.00032&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02&lt;/td&gt;
&lt;td&gt;0.00054&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Even though the dataset mentions to have a configuration with the language of each skeet, I didn't manage to find it.&lt;/p&gt;
&lt;p&gt;But to keep things simple, let's use the &lt;a href="https://github.com/cisnlp/GlotLID"&gt;same model&lt;/a&gt; they did (&lt;code&gt;glotlid&lt;/code&gt;) and keep skeets in English only.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;model_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hf_hub_download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cis-lmu/glotlid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;model.bin&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fasttext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;text_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;text_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text_list&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;is_english&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__label__eng_Latn&amp;quot;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;with_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Series&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_english&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UInt8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;is_english&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;total rows: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;is_english&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;total rows after filtering: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;total rows: 50000
total rows after filtering: 22528
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="a-stupid-simple-vector-store"&gt;A stupid simple vector store&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://lancedb.github.io/lancedb/"&gt;Lancedb&lt;/a&gt; provides a great interface that combines &lt;a href="https://sbert.net/"&gt;sentence-transformers&lt;/a&gt; + pydantic.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lancedb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/tmp/db&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;get_registry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sentence-transformers&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EMBEDDING_MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DEVICE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Skeet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LanceModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SourceField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndims&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VectorField&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_names&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Table was deleted and will be overwritten.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Skeet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# this will automatically add and embed the items.&lt;/span&gt;

&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_dicts&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_fts_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="plain-rag"&gt;Plain RAG&lt;/h2&gt;
&lt;p&gt;In it's most pure form. Retrieval Augmented Generation has 3 simple components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Retrieve context (e.g., search)&lt;/li&gt;
&lt;li&gt;Build prompt&lt;/li&gt;
&lt;li&gt;Answer question&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Skeet&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_pydantic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Skeet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Skeet&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;context_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;context_str&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;======&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

    &lt;span class="n"&gt;PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;* You are an expert at answering questions from the user given a context.&lt;/span&gt;
&lt;span class="s2"&gt;* Use the context section to answer the questions from the user.&lt;/span&gt;
&lt;span class="s2"&gt;* Answer the question directly. No BS.&lt;/span&gt;

&lt;span class="s2"&gt;&amp;lt;context&amp;gt;&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context_str&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;&amp;lt;/context&amp;gt;&lt;/span&gt;

&lt;span class="s2"&gt;&amp;lt;question&amp;gt;&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;&amp;lt;/question&amp;gt;&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;PROMPT&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LLM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;choices&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;message&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;What are the main reasons people are switching from twitter/X to bluesky?&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Question: &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Answer: &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# print(f&amp;quot;Context chunks:\n{context}&amp;quot;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Question:  What are the main reasons people are switching from twitter/X to bluesky?
Answer:
&amp;#39;Based on the context, the main reasons people are switching from Twitter/X to Bluesky seem to be:

1. A desire for a more focused and curated experience, as mentioned in &amp;quot;Trying to be way more active here than Twitter...&amp;quot; and &amp;quot;My favorite thing about Twitter was looking at &amp;#39;What&amp;#39;s Trending&amp;#39;... Does that exist in Bluesky?&amp;quot;
2. Frustration with the chaos and noise on Twitter/X, as implied by &amp;quot;I feel like I’ve been neglecting Bluesky because I been on threads and it’s no different from IG but with Twitter capabilities .. 🤦🏾‍♂️&amp;quot; and &amp;quot;Everything all of the time is not what I ever asked for.&amp;quot;
3. A sense of nostalgia for a more open and community-driven platform, as suggested by comparing Bluesky to Netscape in its early days.
4. A desire to support a more decentralized and less commercial social media platform, as implied by &amp;quot;i like to think of bluesky(tm) as like a netscape . wherein they end up spitting out something cool and popular and open (and - commercial) but do not really find long term business success from that.&amp;quot;&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Great, that works. Now, what can we do more?&lt;/p&gt;
&lt;h2 id="complicating-things-hybrid-search"&gt;Complicating things: Hybrid Search&lt;/h2&gt;
&lt;p&gt;LanceDB also provides a nice hybrid search option - allowing us to use full text search in combination with semantic search. In the default configuration, it will weight vector similarity and full text search around 70-30.&lt;/p&gt;
&lt;p&gt;All we need to do is add &lt;code&gt;query_type='hybrid'&lt;/code&gt;&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;hybrid_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Skeet&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;hybrid&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_pydantic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Skeet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;What are the main reasons people are switching from twitter/X to bluesky?&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hybrid_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Question: &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Answer: &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Question:  What are the main reasons people are switching from twitter/X to bluesky?
Answer:
&amp;#39;Based on the context, the main reasons people are switching from Twitter/X to Bluesky include:

* The ability to see what&amp;#39;s trending and have a better understanding of current events and public discourse
* Frustration with Twitter&amp;#39;s content and moderation policies (as implied by the comment &amp;quot;it doesn&amp;#39;t suck like x&amp;quot;)
* A desire for more nuanced and complex discussions, as evidenced by the quote about people being more complex than we want to assume
* The presence of internet trolls on Twitter, who are also migrating to Bluesky

These factors seem to be driving users to leave Twitter/X and join Bluesky.&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, keep in mind. There are already at least 2 parameters we would need to tune and evaluate here. (1) the number of chunks we want to stuff in the context (here, &lt;code&gt;top_k&lt;/code&gt;), and (2) the type of search we would like to conduct (hybrid, full-text, vector only).&lt;/p&gt;
&lt;p&gt;But if that was not complicated enough, we could complicate a bit more!&lt;/p&gt;
&lt;h2 id="what-if-we-ask-for-a-summary"&gt;What if we ask for a summary?&lt;/h2&gt;
&lt;p&gt;Let's look at the following question:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Give me a high level summary of the skeets&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If we take the naive approach of simply embedding this query, we are likely going to retrieve context with skeets that are similar to our question. At the extreme, this will surface skeets that contain the words high level summary and skeets.&lt;/p&gt;
&lt;p&gt;Now, this might be what we want, but likely isn't. What we want in this case is to provide more context to the LLM regarding topics that align with the overarching content of the skeets, and then let the LLM itself infer the high-level summary from the context.&lt;/p&gt;
&lt;p&gt;The way I think about this is the notion of filters. Here's a quick drawing that illustrates the concept:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/74/summarization.png" alt="RAG question classification" style="max-width:100%;border-radius: 4px"&gt;&lt;/p&gt;
&lt;p&gt;First of all, how do we detect if the user asks such a question?&lt;/p&gt;
&lt;h3 id="detecting-representative-questions"&gt;Detecting representative questions&lt;/h3&gt;
&lt;p&gt;The most basic approach to detecting "representative" questions, is to use an LLM itself to handle them:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;INTENT_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;    Tell me which filter we should use for the given user question.&lt;/span&gt;
&lt;span class="s2"&gt;    The keyword filter filters data based on keywords from the question. This is good for specific questions or when you want to focus on a particular topic.&lt;/span&gt;
&lt;span class="s2"&gt;    The representative filter returns a representative sample of insights, from which you can infer the answer to the question. This is good for questions that relate to the entire dataset (trends, summaries, etc)&lt;/span&gt;
&lt;span class="s2"&gt;    Respond only with the type of filter to use!&lt;/span&gt;

&lt;span class="s2"&gt;    Examples:&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;#39;Summarize the data&amp;#39; -&amp;gt; Representative filter&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;#39;What are the main insights?&amp;#39; -&amp;gt; Representative filter&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;#39;What are the high-level discussion points from our field reps regarding NOS ?&amp;#39; -&amp;gt; Keyword filter&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;#39;Describe the negative, neutral and positive perception of physicians for Benuron?&amp;#39; -&amp;gt; Keyword filter&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;#39;What are the main trends from the following skeets?&amp;#39; -&amp;gt; Representative filter&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;#39;What are the main trends?&amp;#39; -&amp;gt; Representative filter&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;#39;What are the key tweets I should be aware of?&amp;#39; -&amp;gt; Representative filter&lt;/span&gt;

&lt;span class="s2"&gt;    Question: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INTENT_PROMPT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;Summarize the skeets&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;What are the main trends in this dataset?&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;What&amp;#39;s the current weather in Lisbon?&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;Why are people leaving Twitter/X?&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;#39;Summarize the skeets&amp;#39; Representative filter
&amp;#39;What are the main trends in this dataset?&amp;#39; Representative filter
&amp;#39;What&amp;#39;s the current weather in Lisbon?&amp;#39; Keyword filter
&amp;#39;Why are people leaving Twitter/X?&amp;#39; Keyword filter
&amp;#39;Give me a high level summary of the skeets&amp;#39; Representative filter
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The LLM manages to understand if we need a general overview or a specific context quite well.&lt;/p&gt;
&lt;h3 id="answering-questions-that-require-the-entire-dataset-in-context"&gt;Answering questions that require the entire dataset in context&lt;/h3&gt;
&lt;p&gt;There's an interesting article that dives a bit more into this topic &lt;a href="https://pashpashpash.substack.com/p/tackling-the-challenge-of-document"&gt;here&lt;/a&gt;. One idea I stole from there is the idea of passing a 'representative' sample of our dataset to the context.&lt;/p&gt;
&lt;p&gt;Now how can we build a representative sample? Here's one idea:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The user asks a general question that requires the whole dataset in context&lt;/li&gt;
&lt;li&gt;We know we can stuff a maximum of &lt;code&gt;Z&lt;/code&gt; chunks into the context&lt;/li&gt;
&lt;li&gt;We retrieve a large number of embeddings from our dataset at random&lt;/li&gt;
&lt;li&gt;We run K-means clustering or &lt;a href="https://hdbscan.readthedocs.io/en/latest/how_hdbscan_works.html"&gt;something more fancy&lt;/a&gt; to cluster those embeddings into &lt;code&gt;N&lt;/code&gt; clusters/topics&lt;/li&gt;
&lt;li&gt;From each of those topics we draw &lt;code&gt;n&lt;/code&gt; items so that &lt;code&gt;n * N ~= Z&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;We stuff those into the context&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;question&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;#39;Give me a high level summary of the skeets&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_representative_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;max_items_context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# max number of items to stuff in the context&lt;/span&gt;
    &lt;span class="n"&gt;n_clusters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# number of clusters to create&lt;/span&gt;
    &lt;span class="n"&gt;n_items_initial_retrieval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# number of items to retrieve from the database&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Skeet&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Retrieves a representative sample of items from the database to be used as context.&lt;/span&gt;

&lt;span class="sd"&gt;    Args:&lt;/span&gt;
&lt;span class="sd"&gt;        max_items_context (int): The maximum number of items to include in the context.&lt;/span&gt;
&lt;span class="sd"&gt;        n_clusters (int): The number of clusters to create.&lt;/span&gt;
&lt;span class="sd"&gt;        n_items_initial_retrieval (int): The number of items to retrieve from the database.&lt;/span&gt;

&lt;span class="sd"&gt;    Returns:&lt;/span&gt;
&lt;span class="sd"&gt;        list[Skeet]: A list of items from the database.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="n"&gt;items_per_cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_items_context&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n_clusters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;items_per_cluster&lt;/span&gt;&lt;span class="si"&gt;=}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;results_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_pandas&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n_items_initial_retrieval&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results_list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;results_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results_list&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_items_initial_retrieval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;records&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;results_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results_list&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;records&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;results_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Skeet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results_list&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;kmeans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KMeans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_clusters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n_clusters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results_list&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;kmeans&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;cluster_labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kmeans&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;labels_&lt;/span&gt;

    &lt;span class="n"&gt;context_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cluster_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cluster_labels&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;cluster_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cluster_labels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;cluster_id&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cluster_items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cluster_labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cluster_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;context_items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cluster_items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;items_per_cluster&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context_items&lt;/span&gt;

&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_representative_context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Based on the provided context, it appears that there are various &amp;quot;skeets&amp;quot; ( likely referring to Twitter threads or short conversations) scattered throughout the text. However, without further information, I can only provide a general high-level summary:

The skeets seem to be a collection of miscellaneous conversations and comments from various individuals on topics such as:

* Music and celebrities (e.g., Drake, Kendrick)
* Gaming and entertainment (e.g., League of Legends, Arcane)
* Politics and social issues (e.g., Trump, immigration)
* Pop culture references (e.g., references to movies, TV shows, or memes)
* Personal experiences and anecdotes (e.g., someone&amp;#39;s spa date for their John Deere tractor)

These conversations seem to be a mix of humorous, sarcastic, and serious discussions. If you&amp;#39;d like me to provide more specific information about any particular skeet, please let me know!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There are a lot of ways we could make this better. But this is the basic idea.&lt;/p&gt;
&lt;h2 id="expanding-user-queries"&gt;Expanding user queries&lt;/h2&gt;
&lt;p&gt;Another interesting concept is the concept of query expansion. I read this one in the &lt;a href="https://www.amazon.com/LLM-Engineers-Handbook-engineering-production/dp/1836200072"&gt;&lt;em&gt;LLM Engineer's Handbook&lt;/em&gt;&lt;/a&gt;. It's a great book - you should read it to!&lt;/p&gt;
&lt;p&gt;In this case we use an LLM to expand the user query in hope of retrieving even more relevant items into our context. Here's how it works:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;expand_query_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expand_to_n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;

    &lt;span class="c1"&gt;# could be much better by using json mode, structured outputs, or any of these: https://simmering.dev/blog/structured_output/&lt;/span&gt;
    &lt;span class="n"&gt;EXPAND_QUERY_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;* You are an AI language model assistant.&lt;/span&gt;
&lt;span class="s2"&gt;* Your task is to generate &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expand_to_n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; different versions of the given user question to retrieve relevant documents from a vector database.&lt;/span&gt;
&lt;span class="s2"&gt;* By generating multiple perspectives on the user question, your goal is to help the user overcome some of the limitations of the distance-based similarity search.&lt;/span&gt;
&lt;span class="s2"&gt;* Return a json string with a &amp;#39;questions&amp;#39; key, which is a list of strings. It should be parseable by json.loads in Python.&lt;/span&gt;
&lt;span class="s2"&gt;* IMPORTANT: Do not include ```json or any other text in the response.&lt;/span&gt;

&lt;span class="s2"&gt;Original question:&lt;/span&gt;

&lt;span class="s2"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXPAND_QUERY_PROMPT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;questions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;questions&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;questions&lt;/span&gt;

&lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;What are the main reasons people are switching from twitter/X to bluesky?&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;expanded_questions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expand_query_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expanded_questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;- &amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;- &amp;#39;Why did users leave Twitter and join Bluesky?&amp;#39;
- &amp;#39;What factors contribute to the migration of users from X (formerly Twitter) to Bluesky?&amp;#39;
- &amp;#39;Identify the key drivers behind the shift from Twitter to Bluesky, based on user behavior and preferences&amp;#39;
- &amp;#39;What are the primary reasons users are abandoning their old social media accounts on Twitter/X and joining a new platform like Bluesky?&amp;#39;
- &amp;#39;How does the user experience and feature set of Bluesky differ from that of Twitter/X, leading to increased adoption?&amp;#39;
- &amp;#39;What are the main reasons people are switching from twitter/X to bluesky?&amp;#39;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can now take these questions, do a retrieval for each one of them, and answer the question.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expanded_questions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Question: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Answer:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Question: What are the main reasons people are switching from twitter/X to bluesky?
Answer:
Based on the context provided, the main reasons people are switching from Twitter/X to Bluesky include:

1. **Frustration with Twitter/X**: Many users express their dissatisfaction with the current state of Twitter/X, mentioning issues like transphobia, rampant engagement farming, and the platform&amp;#39;s overall toxic environment.
2. **Desire for a more positive experience**: Users are seeking an alternative where they can engage with others in a more constructive and respectful manner. This is evident from statements like &amp;quot;it doesn&amp;#39;t suck like x (twitter)&amp;quot; and &amp;quot;y&amp;#39;all are lovely.&amp;quot;
3. **Wish to preserve the original spirit of Twitter**: Some users nostalgically recall the early days of Twitter, when it was more open, popular, and user-friendly. They hope Bluesky will emulate this spirit.
4. **Attractiveness of new features or improvements**: Although not explicitly stated, some users might be drawn to specific features or improvements that Bluesky offers compared to Twitter/X.

Overall, the main reasons people are switching from Twitter/X to Bluesky seem to be a combination of dissatisfaction with the current state of the former and a desire for a better alternative.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="the-most-important-lesson"&gt;The most important Lesson&lt;/h2&gt;
&lt;p&gt;These are all fancy and nice. But make sure to always start with a baseline. &lt;a href="https://pypi.org/project/rank-bm25/"&gt;A simple one.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="resources-to-go-beyond-the-basics"&gt;Resources to go beyond the basics&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://jxnl.co/writing/category/rag/"&gt;Most things by Jason Liu are great&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arcturus-labs.com/blog/category/retrieval/"&gt;Arcturus labs has some good writing on the topic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/tags/rag/"&gt;Simon Willison's entries are always worth a skim/read&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.amazon.com/LLM-Engineers-Handbook-engineering-production/dp/1836200072"&gt;LLM Engineer's handbook from Paul Iusztin and Maxime Labonne&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.manning.com/books/relevant-search"&gt;I'm currently reading relevant search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.pamelafox.org/2024/09/integrating-vision-into-rag-applications.html"&gt;Handling vision in RAG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://youtu.be/0nA5QG3087g?si=y_JmyCM_4vc59l62"&gt;This talk by Ben Clavié&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://parlance-labs.com/education/rag/"&gt;Most of these talks from Parlance labs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.ragas.io/en/stable/"&gt;Evaluate rag with RAGAS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.anyscale.com/blog/a-comprehensive-guide-for-building-rag-based-llm-applications-part-1"&gt;This blog post by Goku Mohandas&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.infoq.com/news/2023/10/practical-advice-RAG/"&gt;This talk by Sam Partee&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="blog"/></entry><entry><title>An ode to the Marathon</title><link href="https://duarteocarmo.com/blog/the-marathon.html" rel="alternate"/><published>2024-12-17T00:00:00+01:00</published><updated>2024-12-17T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-12-17:/blog/the-marathon.html</id><summary type="html">&lt;p&gt;I'm flying over the Atlantic just between France and Spain for my yearly Christmas trip back to Lisbon. For some reason, I just remembered the blog post I always wanted to write. This one is not about tech, but it's about something I'm equally passionate about: The Marathon.&lt;/p&gt;
&lt;p&gt;Funnily - I'm …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I'm flying over the Atlantic just between France and Spain for my yearly Christmas trip back to Lisbon. For some reason, I just remembered the blog post I always wanted to write. This one is not about tech, but it's about something I'm equally passionate about: The Marathon.&lt;/p&gt;
&lt;p&gt;Funnily - I'm flying pretty close from where my last battle went down: right off the beautiful Cote d'Azure, about 1 month ago, for the &lt;a href="https://cotedazurfrance.com/discover/the-top-events-and-festivals-on-the-cote-dazur/the-alpes-maritimes-nice-cannes-marathon/"&gt;Nice Marathon&lt;/a&gt;, my seventh in ~3 years.&lt;/p&gt;
&lt;p&gt;And if that makes you think that Duarte is probably addicted. You're probably right. But I would argue I'm not addicted to the Marathon itself. I'm addicted to the process.&lt;/p&gt;
&lt;p&gt;Let me explain.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"&lt;em&gt;What's your shoe rotation?&lt;/em&gt;"&lt;/li&gt;
&lt;li&gt;"&lt;em&gt;What gear should I buy?&lt;/em&gt;"&lt;/li&gt;
&lt;li&gt;"&lt;em&gt;Should I join a run club?&lt;/em&gt;"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these miss the point of running completely. Running is a solo sport. And a marathon prep is a tough mental challenge to do well. "&lt;em&gt;I have to work&lt;/em&gt;", "&lt;em&gt;I won't make it this time&lt;/em&gt;", "&lt;em&gt;I'm feeling tired today&lt;/em&gt;". These are just a small taste of the challenge you are really up against: yourself.&lt;/p&gt;
&lt;p&gt;Why would you even? Why would you get up when it's dark and wet outside? Why would you go out for a run when it's the 17th of December on the windy coastline of Denmark? Because you know what lies ahead.&lt;/p&gt;
&lt;p&gt;After those dark and lonely training months the day &lt;em&gt;finally&lt;/em&gt; gets here. Most of us have a superstitious pre-race routine. Something that helps us re-gain the mental strength that things will go according to plan (spoiler alert: They never do). Mine is pretty simple: (1) lay the kit in the ground near the bed, and (2) make sure you have half a dose of Banana chocolate chip &lt;a href="https://www.ekosport.pt/overstims-gatosport-banane-pepites-chocolat-p-9-74741"&gt;Gatosport&lt;/a&gt; ready for next morning.&lt;/p&gt;
&lt;p&gt;Nerves are cracking. Thousands of people lined up with you. If you look closely you can tell: the more to the front people are, the more nervous they are. This is the moment you have been waiting for. There's music, there's announcements, there's noise. A lot of people dance and get excited, I can't bear myself to. The nerves are just too much.&lt;/p&gt;
&lt;p&gt;The start of a Marathon gives me of a feeling I only got in Highschool, when the teacher was walking around the classroom, grading everyone one by one. I was always nervous waiting to hear if I had gotten more than a 10 out of 20.&lt;/p&gt;
&lt;p&gt;All this bullshit, all this music, announcements, all of this dancing around the fire. Now we're going. Some will say the &lt;a href="/blog/run-every-day.html"&gt;inspiration comes from within&lt;/a&gt;. For the first two-thirds of a Marathon, I often find myself looking sideways. Not because I'm &lt;em&gt;comparing&lt;/em&gt; myself to them. But because I can CLEARLY see it! I can see the &lt;em&gt;exact&lt;/em&gt; same pain I'm experiencing. It's a rare moment to be surrounded by a group of people all experiencing the same painful experience at the exact same time. You don't know why they are suffering, why they decided to to this, where they are from, or what they're called - but you know what they're thinking. The same as you.&lt;/p&gt;
&lt;p&gt;Now you've done the hardest part. You've run for 30 Km. You've trained for this: You're ready. Just some of the things I wish I told myself right around that section. The actual thoughts are a mix of "&lt;em&gt;I don't want to do this&lt;/em&gt;", "&lt;em&gt;I should've trained harder&lt;/em&gt;", "&lt;em&gt;I can walk a bit - what's wrong with that&lt;/em&gt;", "&lt;em&gt;I will never run a Marathon again&lt;/em&gt;". Welcome to the wall.&lt;/p&gt;
&lt;p&gt;Even having run a 'respectable' amount of Marathons, the feeling never goes away. Hopefully, you'll plow through. Even if you do start slowing down, even if you lose the pacer or group, you'll keep going.&lt;/p&gt;
&lt;p&gt;"&lt;em&gt;You've already ran 40K! You're not going to stop now!&lt;/em&gt;" I often remember that American lady shouting at me from the sideline in Vienna some years ago. She was most definitely right.&lt;/p&gt;
&lt;p&gt;Yes, those 2 Km now feel like 200. Yes, your legs are tired. But you can see it! You can see that finish line. You made it! Weirdly, you can sprint now; all the pain goes away. It's the culmination of months of effort and hours of pain. This is where you'll probably shed a tear and celebrate with your loved ones.&lt;/p&gt;
&lt;p&gt;Personal-best or not, perfect-race or not. You've done more than 99% of people that day. And that's something to be proud of.&lt;/p&gt;
&lt;p&gt;Now comes the hangover. For some it's a week, for some it's 6 months, for others, it lasts years. No running. No exercise. You've done your part. You've done well. But then… Slowly… You start thinking about it. "&lt;em&gt;It can't be always that painful&lt;/em&gt;", "&lt;em&gt;There are at least 2 or 3 things I could've done better!&lt;/em&gt;", "&lt;em&gt;I'm running already, I should probably do a race...&lt;/em&gt;"&lt;/p&gt;
&lt;p&gt;And that's when you book your next one.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/73/marathon_single.jpg" alt="Vienna Marathon" style="max-width:50%;border-radius: 4px;filter: grayscale(100%)"&gt;
&lt;/center&gt;</content><category term="blog"/></entry><entry><title>Upgrading this website's podcast with F5-TTS</title><link href="https://duarteocarmo.com/blog/podcast-tts-f5-tts-python.html" rel="alternate"/><published>2024-11-12T00:00:00+01:00</published><updated>2024-11-12T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-11-12:/blog/podcast-tts-f5-tts-python.html</id><summary type="html">&lt;p&gt;For the &lt;a href="https://duarteocarmo.com/blog/you-can-now-listen-to-this-blog.html"&gt;past year&lt;/a&gt;, this website's &lt;a href="https://podcasts.apple.com/us/podcast/duarte-o-carmos-articles/id1719493997"&gt;podcast companion&lt;/a&gt; has been running on a text-to-speech model called &lt;a href="https://huggingface.co/coqui/XTTS-v2"&gt;XTTS-v2&lt;/a&gt;. It's not &lt;em&gt;horrible&lt;/em&gt;. And those who have heard my voice before might notice some similarities. But it's far from a &lt;em&gt;pleasant&lt;/em&gt; listenning experience.&lt;/p&gt;
&lt;p&gt;But the world of text-to-speech (TTS) has been gradually moving …&lt;/p&gt;</summary><content type="html">&lt;p&gt;For the &lt;a href="https://duarteocarmo.com/blog/you-can-now-listen-to-this-blog.html"&gt;past year&lt;/a&gt;, this website's &lt;a href="https://podcasts.apple.com/us/podcast/duarte-o-carmos-articles/id1719493997"&gt;podcast companion&lt;/a&gt; has been running on a text-to-speech model called &lt;a href="https://huggingface.co/coqui/XTTS-v2"&gt;XTTS-v2&lt;/a&gt;. It's not &lt;em&gt;horrible&lt;/em&gt;. And those who have heard my voice before might notice some similarities. But it's far from a &lt;em&gt;pleasant&lt;/em&gt; listenning experience.&lt;/p&gt;
&lt;p&gt;But the world of text-to-speech (TTS) has been gradually moving along. &lt;a href="https://notebooklm.google/"&gt;NotebookLM&lt;/a&gt; made headlines, and everyone is sure the future of AI will sit somewhere between agents (whatever those are) and text-to-speech. NotebookLM is an awesome product. But it's still, at its core, a closed source product&lt;sup id="sf-podcast-tts-f5-tts-python-1-back"&gt;&lt;a href="#sf-podcast-tts-f5-tts-python-1" class="simple-footnote" title="There are some open source implementations the core tech is still very much closed source."&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Unfortunately, for state of the art text-to-speech there is still a single undisputed champion out there: &lt;a href="https://elevenlabs.io/"&gt;ElevenLabs&lt;/a&gt;. Or should I say, there &lt;em&gt;was&lt;/em&gt;?&lt;/p&gt;
&lt;p&gt;Late this year a new model from Microsoft came out, called &lt;a href="https://arxiv.org/abs/2406.18009"&gt;E2-TTS&lt;/a&gt;. Sometime after, &lt;a href="https://github.com/SWivid/F5-TTS"&gt;F5-TTS&lt;/a&gt; was released - an improvement over E2 focued on inference and optimization. I was amazed by the &lt;a href="https://www.microsoft.com/en-us/research/project/e2-tts/"&gt;demos&lt;/a&gt; - they sounded &lt;em&gt;crisp&lt;/em&gt; and &lt;em&gt;natural&lt;/em&gt;. After browsing the code for a little bit, it seemed like an easy thing to test out.&lt;/p&gt;
&lt;p&gt;I tested the engine with &lt;a href="https://duarteocarmo.com/blog/around-iceland-6-days-camping-itinerary.html"&gt;one of my latest posts&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://huggingface.co/coqui/XTTS-v2"&gt;XTTS-v2&lt;/a&gt;&lt;/strong&gt;
&lt;audio controls style="width: 100%; display: block" preload="metadata"&gt;&lt;source src="https://r2.duarteocarmo.com/old/old.mp3" type="audio/mpeg"&gt;&lt;/audio&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/SWivid/F5-TTS"&gt;F5-TTS&lt;/a&gt;&lt;/strong&gt;
&lt;audio controls style="width: 100%; display: block" preload="metadata"&gt;&lt;source src="https://r2.duarteocarmo.com/transcripts/10bb68f8f7200f19b71bb095af3c5f32.mp3" type="audio/mpeg"&gt;&lt;/audio&gt;&lt;/p&gt;
&lt;p&gt;I mean, would you just hear that. No more weird accelerations and squeeky voices. The new model is just much more stable and reliable than the previous one. It's light day and night! It's still not perfect though. One might argue the voice gets a bit &lt;em&gt;too&lt;/em&gt; intense some times. The problem with prounounciation persists. It still doesn't know how to say my name properly - but then again - most people also can't.&lt;/p&gt;
&lt;p&gt;I tried getting around the prounounciation by specifying the phoneme sequence&lt;sup id="sf-podcast-tts-f5-tts-python-2-back"&gt;&lt;a href="#sf-podcast-tts-f5-tts-python-2" class="simple-footnote" title="See the demo page for details"&gt;2&lt;/a&gt;&lt;/sup&gt;, but that didn't seem to change much. It did give me a good idea to &lt;a href="https://github.com/duarteocarmo/podcaster/blob/master/src/podcaster/parser.py#L109"&gt;preprocess&lt;/a&gt; the audio with an LLM - and that also makes things smoother.&lt;/p&gt;
&lt;p&gt;Running F5-TTS was also trivial thanks to Modal's great product (still haven't paid a penny I must say, but would happily). Only this specific function needs to run on a GPU, and it does so just by using &lt;a href="https://modal.com/docs/guide/gpu"&gt;Modal's decorator&lt;/a&gt;:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;gpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODAL_GPU&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mounts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;modal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_local_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;LOCAL_DATA_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remote_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODAL_REMOTE_DATA_DIR&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ParsedArticle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reference_voice_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REFERENCE_VOICE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;reference_text_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REFERENCE_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MODEL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference_text_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"r"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;voice_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reference_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;voice_text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"f5-tts_infer-cli"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"--model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"--ref_audio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reference_voice_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"--ref_text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reference_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"--gen_text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_for_tts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;target_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tests/infer_cli_out.wav"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"File &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target_file&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not exist."&lt;/span&gt;

    &lt;span class="n"&gt;mp3_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;convert_to_mp3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mp3_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"rb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;audio_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;audio_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;audio_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;audio_bytes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you're interested in thre rest of the code, &lt;a href="https://github.com/duarteocarmo/podcaster"&gt;Podcaster&lt;/a&gt; is free and open source.&lt;/p&gt;
&lt;hr&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-podcast-tts-f5-tts-python-1"&gt;There are &lt;a href="https://github.com/lucidrains/soundstorm-pytorch"&gt;some&lt;/a&gt; &lt;a href="https://github.com/meta-llama/llama-recipes/tree/main/recipes/quickstart/NotebookLlama"&gt;open&lt;/a&gt; &lt;a href="https://huggingface.co/spaces/gabrielchua/open-notebooklm"&gt;source&lt;/a&gt; implementations the core tech is still very much closed source. &lt;a href="#sf-podcast-tts-f5-tts-python-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;li id="sf-podcast-tts-f5-tts-python-2"&gt;See the &lt;a href="https://www.microsoft.com/en-us/research/project/e2-tts/"&gt;demo page&lt;/a&gt; for details &lt;a href="#sf-podcast-tts-f5-tts-python-2-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>What if ChatGPT's memory was a knowledge graph?</title><link href="https://knowledgegraphchat.duarteocarmo.com/" rel="alternate"/><published>2024-10-29T00:00:00+01:00</published><updated>2024-10-29T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:knowledgegraphchat.duarteocarmo.com,2024-10-29:/</id><content type="html"/><category term="blog"/></entry><entry><title>Classification in the age of LLMs: The emoji problem</title><link href="https://duarteocarmo.com/blog/classification-llms-emoji-open-source-ollama.html" rel="alternate"/><published>2024-10-07T00:00:00+02:00</published><updated>2024-10-07T00:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-10-07:/blog/classification-llms-emoji-open-source-ollama.html</id><summary type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/70/header_169.png" alt="Fruits and bowl" style="max-width:100%;border-radius: 2px;"&gt;
&lt;/center&gt;

&lt;p&gt;For the past years Vitto and I have used &lt;a href="https://tricount.com/"&gt;Tricount&lt;/a&gt; to track our shared expenses. The app is actually pretty good, but there’s one small thing that annoys me quite a bit.&lt;/p&gt;
&lt;p&gt;Even though we spoke English to each other for the first month, we’ve since spoken a …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/70/header_169.png" alt="Fruits and bowl" style="max-width:100%;border-radius: 2px;"&gt;
&lt;/center&gt;

&lt;p&gt;For the past years Vitto and I have used &lt;a href="https://tricount.com/"&gt;Tricount&lt;/a&gt; to track our shared expenses. The app is actually pretty good, but there’s one small thing that annoys me quite a bit.&lt;/p&gt;
&lt;p&gt;Even though we spoke English to each other for the first month, we’ve since spoken a mix of Italian and Portuguese. And when I say &lt;em&gt;a mix&lt;/em&gt;, I really mean &lt;em&gt;a mix&lt;/em&gt;. I mean, would you just look at our expense tracking app:&lt;/p&gt;
&lt;div style="display: flex; justify-content: center; gap: 10px; width: 100%; max-width: 100%;"&gt;
  &lt;img src="https://duarteocarmo.com/images/70/tricount_1.png" alt="Image 1" style="max-width: calc(50% - 5px); height: auto; flex: 1;"&gt;
  &lt;img src="https://duarteocarmo.com/images/70/tricount_2.png" alt="Image 2" style="max-width: calc(50% - 5px); height: auto; flex: 1;"&gt;
&lt;/div&gt;

&lt;p&gt;For the average calm and relaxed reader, this might not seem like much. But for us, other folk, it’s extremely annoying to see how it misses assigning the right emojis to most of our transactions! It gets Beers (🍺), it gets when we speak English and write something like “Train” (🚂). But whenever we speak something in Portuguese or Italian, BAM, there comes the generic 💶 emoji.&lt;/p&gt;
&lt;p&gt;Now. Is this a big problem? No. Is it an annoying problem? Yes. Is this a solved problem? &lt;em&gt;Probably&lt;/em&gt;. But in the age of LLMs and 400 Billion parameters models, what options do we have?&lt;/p&gt;
&lt;h2 id="creating-a-labeled-dataset-from-screenshots"&gt;Creating a labeled dataset from screenshots&lt;/h2&gt;
&lt;p&gt;Before looking at all the ways the Tricount emoji classification could be better, we should first gather a reasonable dataset. What before required some &lt;em&gt;grueling&lt;/em&gt; hand labelling or some clunky OCR library, now ‘only’ requires a couple of calls to &lt;code&gt;gpt-4o&lt;/code&gt;.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Define data structure&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ExpenseItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;payer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Expenses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ExpenseItem&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Encode the images in base64&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;encode_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;rb&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;image_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;utf-8&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;images_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;image_url&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;image_url&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;data:image/png;base64,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;encode_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ima&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ima&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;../tricount_pictures&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*.PNG&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Classify&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;beta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;List all the expenses in a json format&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;images_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Expenses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;expenses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;

&lt;span class="c1"&gt;# prints&lt;/span&gt;

&lt;span class="c1"&gt;#[ExpenseItem(name=&amp;#39;Beers&amp;#39;, amount=XXX, currency=&amp;#39;DKK&amp;#39;, payer=&amp;#39;Dudu (me)&amp;#39;, emoji=&amp;#39;🍺&amp;#39;),&lt;/span&gt;

&lt;span class="c1"&gt;# ExpenseItem(name=&amp;#39;Beers&amp;#39;, amount=XXX, currency=&amp;#39;DKK&amp;#39;, payer=&amp;#39;Dudu (me)&amp;#39;, emoji=&amp;#39;🍺&amp;#39;),&lt;/span&gt;

&lt;span class="c1"&gt;# ExpenseItem(name=&amp;#39;Decatlon comun&amp;#39;, amount=XXX, currency=&amp;#39;DKK&amp;#39;, payer=&amp;#39;Vitto&amp;#39;, emoji=&amp;#39;💶&amp;#39;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Just liked that, we now have a dataset of ~50 expenses. Great. Let’s get to classifying.&lt;/p&gt;
&lt;h2 id="classification-using-gpt-4o-and-structured-outputs"&gt;Classification using &lt;code&gt;gpt-4o&lt;/code&gt; and structured outputs&lt;/h2&gt;
&lt;p&gt;Before we do anything else, how do the &lt;em&gt;big guns&lt;/em&gt; perform? What if we just ask &lt;code&gt;gpt-4o&lt;/code&gt; to classify? This is the simplest of options, even simpler now that OpenAI directly supports Pydantic base models via structured outputs. We can solve this problem in a couple dozen lines of code:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# define pydantic model&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;EmojiClassification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    A single emoji that describes a financial transaction (e.g., Airplane ticket -&amp;gt; ✈️)&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;The emoji that describes the transaction (Must be a single character!)&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@field_validator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;emoji&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;validate_emoji&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_emoji&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;The emoji must be a single character! Received: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;

&lt;span class="c1"&gt;# classification function&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;classify_openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Classify this transaction: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;beta&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gpt-4o-mini-2024-07-18&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;response_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EmojiClassification&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emoji&lt;/span&gt;

&lt;span class="n"&gt;classify_openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Treno Milano Ancona&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# prints &amp;#39;🚆&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That was easy. But also not particularly exciting. What if our data is sensitive? What if our API gets extremely popular? What if we just want to handle it &lt;em&gt;ourselves&lt;/em&gt;?  Could we do the same with some off-the-shelf open-source models?&lt;/p&gt;
&lt;h2 id="function-calling-local-models-with-ollama"&gt;Function calling local models with Ollama&lt;/h2&gt;
&lt;p&gt;I would love to tell you that open-source function calling was a solved problem. And yes - the &lt;a href="https://gorilla.cs.berkeley.edu/"&gt;function calling leaderboard&lt;/a&gt; certainly seems to tell that story. But the reality of local models is &lt;em&gt;a bit&lt;/em&gt; different.&lt;/p&gt;
&lt;p&gt;Here’s the code to accomplish the same thing OpenAI models can do with an &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt; model that supports function calling:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Eats pydantic model, shoots &amp;#39;json schema&amp;#39;&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;kw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;annotation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Parameter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Input for `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;function&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;function&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__doc__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;parameters&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Don&amp;#39;t even try without this (email me if you have a better option!)&lt;/span&gt;

&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;You are a genius expert. Your task is to classify the description of a financial transaction with an emoji.&lt;/span&gt;
&lt;span class="s2"&gt;You must only use emojis that are a single character.&lt;/span&gt;
&lt;span class="s2"&gt;You must only reply with a single emoji.&lt;/span&gt;

&lt;span class="s2"&gt;Example 1:&lt;/span&gt;
&lt;span class="s2"&gt;User: Treno Milano Ancona&lt;/span&gt;
&lt;span class="s2"&gt;Response: EmojiClassification(emoji=&amp;#39;🚆&amp;#39;)&lt;/span&gt;

&lt;span class="s2"&gt;Example 2:&lt;/span&gt;
&lt;span class="s2"&gt;User: Dinner with friends&lt;/span&gt;
&lt;span class="s2"&gt;Response: EmojiClassification(emoji=&amp;#39;🍽&amp;#39;)&lt;/span&gt;

&lt;span class="s2"&gt;Example 3:&lt;/span&gt;
&lt;span class="s2"&gt;User: Plane ticket to New York&lt;/span&gt;
&lt;span class="s2"&gt;Response: EmojiClassification(emoji=&amp;#39;✈️&amp;#39;)&lt;/span&gt;

&lt;span class="s2"&gt;Use the EmojiClassification tool to help you classify the transactions.&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# This is a bad example of retry logic - but you get the point&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;classify_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;expense_description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EmojiClassification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;classification_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;User: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expense_description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

        &lt;span class="c1"&gt;# poor man&amp;#39;s retry&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;retry_num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
          &lt;span class="c1"&gt;# we use OpenAI&amp;#39;s client&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;system&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;classification_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmojiClassification&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;retry_num&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;tool_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;tool_calls&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;EmojiClassification&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Failed to classify expense. Expected EmojiClassification but got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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="n"&gt;tool_calls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;None&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EmojiClassification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emoji&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ValidationError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Validation error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

&lt;span class="c1"&gt;# we can now call this with ollama&lt;/span&gt;

&lt;span class="n"&gt;classify_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;expense_description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Donut&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;llama3.1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;http://localhost:11434/v1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ollama&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# dummy key - required&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# prints &amp;#39;🍩&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;details&gt;
  &lt;summary&gt;Note: Using Instructor to accomplish the same &lt;/summary&gt;

A kind user on &lt;a href=https://x.com/molasalex/status/1843553936407703666&gt;twitter&lt;/a&gt; reached out mentioning you could do the same with &lt;a href=https://python.useinstructor.com/&gt;Instructor&lt;/a&gt;

I got a lot of Validation errors at the time, so I did not go this route, the code gets a lot simpler, so worth the test!


&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;classify_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;expense_description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;http://localhost:11434/v1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;ollama&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EmojiClassification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;instructor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;instructor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TOOLS_STRICT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;classification_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;User: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expense_description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;system&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;classification_prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EmojiClassification&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;/details&gt;

&lt;p&gt;Perfect, we can now use any local Ollama model that supports function calling and classify our expense description as well.&lt;/p&gt;
&lt;p&gt;Why don’t we try some old-school machine learning?&lt;/p&gt;
&lt;h2 id="embeddings-for-classification"&gt;Embeddings for classification&lt;/h2&gt;
&lt;p&gt;Embeddings are another option. Open source, fast, and lightweight. Here’s a possible logic:  given a list of emoji and their descriptions, as well as the description of an expense, return the ‘most similar’ emoji description to the expense.&lt;/p&gt;
&lt;p&gt;Easier in code than in English:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# load a small db of emojis and descriptions&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;./emojis.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;emoji&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# prepare column to be embedded (I hate this)&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;to_embed&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;passage: &amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# embed it (multilingual embedding, since the description are in IT and PT)&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sentence_transformers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;intfloat/multilingual-e5-base&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;to_embed&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;show_progress_bar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mps&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;# create a lancedb table with the data&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lancedb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.my_db&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;table_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;emojis&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;records&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Created table &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Why not try a re-ranker as well?&lt;/span&gt;

&lt;span class="n"&gt;ranker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Reranker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;nreimers/mmarco-mMiniLMv2-L12-H384-v1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Create a classification function&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;embedding_classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;expense_description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# number of closest emoji to re-rank&lt;/span&gt;
    &lt;span class="n"&gt;use_reranker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;query_embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;query: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expense_description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;show_progress_bar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mps&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_pandas&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;use_reranker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;emoji&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ranker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;expense_description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;doc_ids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;emoji&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding_classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Beers and pizza with friends&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_reranker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# prints 🍕&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding_classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Almoco&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_reranker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# prints ⚗&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Great, both the vanilla and re-ranked version of the function appear to work correctly. We now at least three ways of classifying the description of an expense.&lt;/p&gt;
&lt;h2 id="benchmarking-expense-categorization"&gt;Benchmarking expense categorization&lt;/h2&gt;
&lt;p&gt;We now have all the functions we need. With the code below, we now run a simple benchmark. For the 50 descriptions of expenses we gathered for our dataset, we run a set of different techniques/models.&lt;/p&gt;
&lt;p&gt;I didn’t test all models in the world. I tested &lt;code&gt;gpt-4o&lt;/code&gt;, 3-4 local models I usually run on my laptop, and the embedding techniques. Here’s the benchmarking code and results:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;CACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ExpenseItem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CACHE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;CACHE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;tricount_emoji&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;openai&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;classify_openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;llama3.1_7b: &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;classify_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;llama3.1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;api_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;qwen2.5_3b&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;classify_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;qwen2.5:3b&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;api_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;qwen2.5_1.5b&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;classify_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;qwen2.5:3b&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;api_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;llama3.2_3b&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;classify_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;llama3.2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;api_params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;embedding&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;embedding_classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_reranker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;embedding_reranker&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;embedding_classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_reranker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;CACHE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;expense&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expenses&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;bench_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bench_df&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;description&lt;/th&gt;
&lt;th&gt;tricount_emoji&lt;/th&gt;
&lt;th&gt;openai&lt;/th&gt;
&lt;th&gt;llama3.1_7b&lt;/th&gt;
&lt;th&gt;qwen2.5_3b&lt;/th&gt;
&lt;th&gt;qwen2.5_1.5b&lt;/th&gt;
&lt;th&gt;llama3.2_3b&lt;/th&gt;
&lt;th&gt;embedding&lt;/th&gt;
&lt;th&gt;embedding_reranker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Beers&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍻&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beers&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍺&lt;/td&gt;
&lt;td&gt;🍻&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decatlon comun&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🛒&lt;/td&gt;
&lt;td&gt;🏃&lt;/td&gt;
&lt;td&gt;🛍&lt;/td&gt;
&lt;td&gt;🛍&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🌳&lt;/td&gt;
&lt;td&gt;📨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duarte decatlon&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🏃‍♂️&lt;/td&gt;
&lt;td&gt;🏃&lt;/td&gt;
&lt;td&gt;🛍&lt;/td&gt;
&lt;td&gt;🛍&lt;/td&gt;
&lt;td&gt;🏦&lt;/td&gt;
&lt;td&gt;🌳&lt;/td&gt;
&lt;td&gt;🌳&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Festa&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🎉&lt;/td&gt;
&lt;td&gt;🎉&lt;/td&gt;
&lt;td&gt;🎉&lt;/td&gt;
&lt;td&gt;🎉&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🎭&lt;/td&gt;
&lt;td&gt;🎑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jantar&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🍽️&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;👩‍🍳&lt;/td&gt;
&lt;td&gt;👩‍🍳&lt;/td&gt;
&lt;td&gt;🏯&lt;/td&gt;
&lt;td&gt;⚱&lt;/td&gt;
&lt;td&gt;⚱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Almoco&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🍽️&lt;/td&gt;
&lt;td&gt;🍏&lt;/td&gt;
&lt;td&gt;🍔&lt;/td&gt;
&lt;td&gt;🍔&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;⚗&lt;/td&gt;
&lt;td&gt;⚗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jantar&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🍽️&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;👩‍🍳&lt;/td&gt;
&lt;td&gt;👩‍🍳&lt;/td&gt;
&lt;td&gt;🏯&lt;/td&gt;
&lt;td&gt;⚱&lt;/td&gt;
&lt;td&gt;⚱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hotel Bolo&lt;/td&gt;
&lt;td&gt;🏨&lt;/td&gt;
&lt;td&gt;🏨&lt;/td&gt;
&lt;td&gt;🏨&lt;/td&gt;
&lt;td&gt;🏨&lt;/td&gt;
&lt;td&gt;🏨&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🏨&lt;/td&gt;
&lt;td&gt;🏚&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treno Milano Ancona&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🚆&lt;/td&gt;
&lt;td&gt;🚂&lt;/td&gt;
&lt;td&gt;🚆&lt;/td&gt;
&lt;td&gt;🚆&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🚄&lt;/td&gt;
&lt;td&gt;🚡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cena vigana milano&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🍽️&lt;/td&gt;
&lt;td&gt;🍴&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🌖&lt;/td&gt;
&lt;td&gt;🌐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Train venezia milano&lt;/td&gt;
&lt;td&gt;🚂&lt;/td&gt;
&lt;td&gt;🚆&lt;/td&gt;
&lt;td&gt;🚂&lt;/td&gt;
&lt;td&gt;🚆&lt;/td&gt;
&lt;td&gt;🚆&lt;/td&gt;
&lt;td&gt;🚂&lt;/td&gt;
&lt;td&gt;🚄&lt;/td&gt;
&lt;td&gt;🚆&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reimbursement&lt;/td&gt;
&lt;td&gt;💳&lt;/td&gt;
&lt;td&gt;💸&lt;/td&gt;
&lt;td&gt;💸&lt;/td&gt;
&lt;td&gt;⛺&lt;/td&gt;
&lt;td&gt;⛺&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🚑&lt;/td&gt;
&lt;td&gt;🎁&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spritz&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🍹&lt;/td&gt;
&lt;td&gt;🍹&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🥨&lt;/td&gt;
&lt;td&gt;✨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mozzarella&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🧀&lt;/td&gt;
&lt;td&gt;🧀&lt;/td&gt;
&lt;td&gt;🧀&lt;/td&gt;
&lt;td&gt;🧀&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🍝&lt;/td&gt;
&lt;td&gt;🍄&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spritz&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🍹&lt;/td&gt;
&lt;td&gt;🍹&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🥨&lt;/td&gt;
&lt;td&gt;✨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Barca&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;⚽&lt;/td&gt;
&lt;td&gt;⚽&lt;/td&gt;
&lt;td&gt;🏀&lt;/td&gt;
&lt;td&gt;🏀&lt;/td&gt;
&lt;td&gt;🚣&lt;/td&gt;
&lt;td&gt;🔖&lt;/td&gt;
&lt;td&gt;📶&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kwikly&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🛒&lt;/td&gt;
&lt;td&gt;💳&lt;/td&gt;
&lt;td&gt;🏃&lt;/td&gt;
&lt;td&gt;🏃&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🥝&lt;/td&gt;
&lt;td&gt;🥝&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spesa&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;💳&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;💰&lt;/td&gt;
&lt;td&gt;💰&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;♠&lt;/td&gt;
&lt;td&gt;🌾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voli Tokyo&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;✈️&lt;/td&gt;
&lt;td&gt;💖&lt;/td&gt;
&lt;td&gt;✈️&lt;/td&gt;
&lt;td&gt;✈️&lt;/td&gt;
&lt;td&gt;✂&lt;/td&gt;
&lt;td&gt;🗼&lt;/td&gt;
&lt;td&gt;🗼&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conguaglio voli Zurigo&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;✈️&lt;/td&gt;
&lt;td&gt;✈️&lt;/td&gt;
&lt;td&gt;✈️&lt;/td&gt;
&lt;td&gt;✈️&lt;/td&gt;
&lt;td&gt;✈&lt;/td&gt;
&lt;td&gt;🔄&lt;/td&gt;
&lt;td&gt;🌐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amar&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;👤&lt;/td&gt;
&lt;td&gt;💸&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🤑&lt;/td&gt;
&lt;td&gt;🕉&lt;/td&gt;
&lt;td&gt;💢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groceries&lt;/td&gt;
&lt;td&gt;🛒&lt;/td&gt;
&lt;td&gt;🛒&lt;/td&gt;
&lt;td&gt;🛍&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🛍&lt;/td&gt;
&lt;td&gt;🥫&lt;/td&gt;
&lt;td&gt;🍲&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durum&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🍞&lt;/td&gt;
&lt;td&gt;🥖&lt;/td&gt;
&lt;td&gt;🍞&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;📦&lt;/td&gt;
&lt;td&gt;🥁&lt;/td&gt;
&lt;td&gt;🛢&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groceries&lt;/td&gt;
&lt;td&gt;🛒&lt;/td&gt;
&lt;td&gt;🛒&lt;/td&gt;
&lt;td&gt;🛍&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🛍&lt;/td&gt;
&lt;td&gt;🥫&lt;/td&gt;
&lt;td&gt;🍲&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Money transfer&lt;/td&gt;
&lt;td&gt;💳&lt;/td&gt;
&lt;td&gt;💸&lt;/td&gt;
&lt;td&gt;💸&lt;/td&gt;
&lt;td&gt;💸&lt;/td&gt;
&lt;td&gt;💸&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;💰&lt;/td&gt;
&lt;td&gt;🤑&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lunch&lt;/td&gt;
&lt;td&gt;🍜&lt;/td&gt;
&lt;td&gt;🍔&lt;/td&gt;
&lt;td&gt;🍏&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🍽&lt;/td&gt;
&lt;td&gt;🥪&lt;/td&gt;
&lt;td&gt;🕐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cafe&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;☕&lt;/td&gt;
&lt;td&gt;☕&lt;/td&gt;
&lt;td&gt;☕&lt;/td&gt;
&lt;td&gt;☕&lt;/td&gt;
&lt;td&gt;☕&lt;/td&gt;
&lt;td&gt;☕&lt;/td&gt;
&lt;td&gt;🔪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spesa kwikly&lt;/td&gt;
&lt;td&gt;💶&lt;/td&gt;
&lt;td&gt;🛒&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;🛒&lt;/td&gt;
&lt;td&gt;🛒&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🐳&lt;/td&gt;
&lt;td&gt;💪&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pizza&lt;/td&gt;
&lt;td&gt;🍕&lt;/td&gt;
&lt;td&gt;🍕&lt;/td&gt;
&lt;td&gt;🍕&lt;/td&gt;
&lt;td&gt;🍕&lt;/td&gt;
&lt;td&gt;🍕&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;🍕&lt;/td&gt;
&lt;td&gt;🍕&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Note: &lt;code&gt;None&lt;/code&gt; means the model did not call the appropriate function, and thus failed to produce a classification.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Looking at the results, we can observe a couple of interesting things:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OpenAI’s &lt;code&gt;gpt4-o&lt;/code&gt; is hard to beat.&lt;/strong&gt; It pretty much &lt;em&gt;never&lt;/em&gt; missed. Even when Vitto was too lazy to write the rest of the description. My guilty pleasure shopping at decathlon? Got it. &lt;em&gt;Barca&lt;/em&gt; was actually referring to &lt;em&gt;boat&lt;/em&gt; - and not the actual football club - there’s no way it could’ve figured that one out. Hard to beat. &lt;em&gt;Kwikly&lt;/em&gt; refers to a local supermarket we go to in Copenhagen - and that looks like a store emoji to me!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The bigger the open model, the better the results.&lt;/strong&gt; And Llama 3 shows it. It performs pretty well, especially at the 7b size. Not without hiccups, of course (can’t figure out how 💖 relates to flights?).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Small open source models are getting good.&lt;/strong&gt; Not sure what happened to Llama 3.2 3b - but at 3 and 1.5b sizes, Alibaba’s Qwen2 family of models perform surprisingly well. Some bigger problems &lt;em&gt;do&lt;/em&gt; show up. We also see a lot of failures to call the right function. (hence the &lt;code&gt;None&lt;/code&gt;s). I’m still curious about two things. (1) How much better could we get through prompting. (2) How much better larger open source models perform (think 70B or 400B). I can’t run those myself, so it’s beside the point of this post.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The embedding idea sounded interesting, but didn’t really perform.&lt;/strong&gt; We would probably have to do a bit more work than just a cosine similarity on the emoji descriptions. Even though the embeddings were multilingual, they clearly didn’t capture the essence of the expense. Another approach I would test is to create a labeled dataset with &lt;code&gt;gpt-4o&lt;/code&gt; and then a simple random forest on embeddings. But again, beside the point of this post.&lt;/p&gt;
&lt;p&gt;In any case, we can for sure do better than what Tricount is currently doing!&lt;/p&gt;
&lt;hr /&gt;
&lt;h2 id="epilogue-just-get-the-right-token"&gt;Epilogue: Just get the right token&lt;/h2&gt;
&lt;p&gt;Sorry, I couldn’t resist. I need to test a last one. OpenAI has a cool feature called &lt;a href="https://cookbook.openai.com/examples/using_logprobs"&gt;log probs&lt;/a&gt; in their API that allows you to run classification by querying by which token has the highest probability in the completion.&lt;/p&gt;
&lt;p&gt;Unfortunately, &lt;a href="https://github.com/ollama/ollama/issues/2415"&gt;Ollama doesn’t seem to support this&lt;/a&gt;. But I figured how to do with plain Transformers. Not the most efficient, since I have to run for every possible emoji. But do let me know if you (reader) have better suggestions:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;transformers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;

&lt;span class="n"&gt;model_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;meta-llama/Llama-3.2-1B-Instruct&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForCausalLM&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# log probability of sentence&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;calculate_sentence_log_probability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;outputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;logits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:]&lt;/span&gt;  &lt;span class="c1"&gt;# Align token predictions&lt;/span&gt;
    &lt;span class="n"&gt;shifted_input_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;  &lt;span class="c1"&gt;# Drop first token&lt;/span&gt;

    &lt;span class="n"&gt;log_probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log_softmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;token_log_probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log_probs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shifted_input_ids&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unsqueeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;squeeze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;token_log_probs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# wrapper to run through the emojis&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;find_most_likely_emoji&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_sentence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emoji_list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;log_probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;emoji&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;emoji_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sentence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_sentence&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;log_probs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calculate_sentence_log_probability&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;most_likely_emoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_probs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;log_probs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;most_likely_emoji&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log_probs&lt;/span&gt;

&lt;span class="c1"&gt;# prompt&lt;/span&gt;

&lt;span class="n"&gt;USER_MESSAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;You are a genius expert. Your task is to classify the description of a financial transaction with an emoji.&lt;/span&gt;
&lt;span class="s2"&gt;You must only use emojis that are a single character.&lt;/span&gt;
&lt;span class="s2"&gt;You must only reply with a single emoji.&lt;/span&gt;

&lt;span class="s2"&gt;Example 1:&lt;/span&gt;
&lt;span class="s2"&gt;User: Treno Milano Ancona&lt;/span&gt;
&lt;span class="s2"&gt;Response: 🚆&lt;/span&gt;
&lt;span class="s2"&gt;...&lt;/span&gt;

&lt;span class="s2"&gt;Now it&amp;#39;s your turn! Classify the following transactions with an emoji:&lt;/span&gt;
&lt;span class="s2"&gt;User: &lt;/span&gt;&lt;span class="si"&gt;{description}&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;USER_MESSAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;USER_MESSAGE&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Voli Tokyo&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# &amp;#39;Flights Tokyo&amp;#39;&lt;/span&gt;

&lt;span class="c1"&gt;# massaging the formats and tokenizing&lt;/span&gt;

&lt;span class="n"&gt;ASSISTANT_MESSAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;Response: &lt;/span&gt;&lt;span class="si"&gt;{emoji}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;user&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;USER_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;assistant&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ASSISTANT_MESSAGE&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply_chat_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokenize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;base_sentence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# classification&lt;/span&gt;

&lt;span class="n"&gt;emoji_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;🎄&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;🔔&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;🚨&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;💎&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;🇬🇹&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;🍹&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;🚣&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;⚽&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;🥘&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;🛩️&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;🇯🇵&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;✈️&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# example possibilities&lt;/span&gt;
&lt;span class="n"&gt;most_likely_emoji&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log_probs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_most_likely_emoji&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_sentence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emoji_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Most likely emoji: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;most_likely_emoji&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;log_prob&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;log_probs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Log probability of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;log_prob&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Most likely emoji: ✈️&lt;/span&gt;

&lt;span class="c1"&gt;# Log probability of 🎄: -582.9215087890625&lt;/span&gt;

&lt;span class="c1"&gt;# Log probability of 🔔: -580.827392578125&lt;/span&gt;

&lt;span class="c1"&gt;# Log probability of 🚨: -577.4615478515625&lt;/span&gt;

&lt;span class="c1"&gt;# Log probability of 💎: -582.4413452148438&lt;/span&gt;

&lt;span class="c1"&gt;# Log probability of 🇬🇹: -580.4923095703125&lt;/span&gt;

&lt;span class="c1"&gt;# Log probability of 🍹: -581.0596923828125&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Works well!&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Around Iceland in 6 days</title><link href="https://duarteocarmo.com/blog/around-iceland-6-days-camping-itinerary.html" rel="alternate"/><published>2024-08-30T15:30:00+02:00</published><updated>2024-08-30T15:30:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-08-30:/blog/around-iceland-6-days-camping-itinerary.html</id><summary type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/69/landscape.png" alt="Icelandic Landscape" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;For a short summer break, Vitto challenged me to visit Iceland with her. As someone that is used to going south for the summer - this sounded stupid at first, but I was &lt;em&gt;incredibly&lt;/em&gt; surprised. For 6 days, we were pretty much in another planet. Here are some notes and tips …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/69/landscape.png" alt="Icelandic Landscape" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;For a short summer break, Vitto challenged me to visit Iceland with her. As someone that is used to going south for the summer - this sounded stupid at first, but I was &lt;em&gt;incredibly&lt;/em&gt; surprised. For 6 days, we were pretty much in another planet. Here are some notes and tips from our trip.&lt;/p&gt;
&lt;h2 id="minimal-pre-trip-preparations"&gt;Minimal pre-trip preparations&lt;/h2&gt;
&lt;p&gt;We only did two things in preparation for the trip. The first was renting out our home for the week. After some googling we decided to go with &lt;a href="https://www.kukucampers.is/"&gt;Kuku Campers&lt;/a&gt;. It’s a ‘local’ shop, ran mostly by summer working EU immigrants. Price was reasonable, staff was friendly. No complaints.&lt;/p&gt;
&lt;p&gt;The second most important preparation is the most critical. It’s a small trick I learned 4 years ago from my friend Alex. That little trick is called &lt;em&gt;&lt;a href="https://www.amazon.fr/Guide-Routard-Islande-2024-25/dp/2017888397/ref=sr_1_1?dib=eyJ2IjoiMSJ9.MBhiA55sDoSmJRDHLdCaoFNDtWciwNpW3SO7B2P9O8VEC35Mgt6SIiZlO5-H3t6hIg5cYtxZgYfIxbwm_DF3b_CoNFUHz3BhhiAtRd50Iz8rJA7Ak1CH_g0UZJw05ahkilvyujQkRD6weKxwJsMjZDvMttgk_Bp1Pa97vDQMQYtX8ZLyQ4aIhJByrEA-FznfPvUveIIbRLk1d9Tvrt52Wf81sy7Aa_Nrnoz8xoUbRA0.OIxFW121aSdMTzierOgfEtop-VwDN0CT9EBz_dTrDog&amp;amp;dib_tag=se&amp;amp;keywords=Guide+Routard+Islande&amp;amp;qid=1724441068&amp;amp;s=books&amp;amp;sr=1-1"&gt;Le Petit Routard&lt;/a&gt;&lt;/em&gt;. We bought the Iceland 2024 Edition. As always, it did &lt;em&gt;not&lt;/em&gt; disappoint. It’s just like traveling around with a local guide. And there’s just something about the French way of &lt;em&gt;routard&lt;/em&gt; travel that has always stuck with me. You won’t know until you’ve tried it.&lt;/p&gt;
&lt;center&gt;
&lt;iframe src="https://www.google.com/maps/d/u/1/embed?mid=15DR367Ywerql0T_GuuHBby5EN4nrgUI&amp;ehbc=2E312F" style="width: 100%; height: 480px; max-width: 100%;" allowfullscreen&gt;&lt;/iframe&gt;
&lt;/center&gt;

&lt;h2 id="day-1-lift-off-golden-triangle"&gt;Day 1: Lift off &amp;amp; Golden Triangle&lt;/h2&gt;
&lt;p&gt;We woke up early in Reykjavik, had breakfast at the &lt;a href="https://www.centerhotels.com/en/hotel-plaza-reykjavik"&gt;Center Hotels Plaza&lt;/a&gt;. Once done, we went back to the Airport region of Keflavik and picked up our camping van. We set way towards the first ‘&lt;em&gt;coup the coeur&lt;/em&gt;’ from the Routard: the Golden Triangle Region.&lt;/p&gt;
&lt;p&gt;We managed to visit the place where the two tectonic plates meet, the &lt;a href="https://www.thingvellir.is/en/"&gt;Þingvellir national park&lt;/a&gt;. Which brought me some good memories of Geological walks with my grandparents. Also managed to quickly hit the original &lt;em&gt;Geysir&lt;/em&gt; (reminded me often of what we see in the Azores region). Except Icelandic people don’t &lt;a href="https://youtu.be/F9piJgQKqRQ?si=ip08U8DCYfLfI1Uk"&gt;cook underground like us&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Closed off Day 1 with our first &lt;a href="https://g.co/kgs/cLGDmwS"&gt;campsite&lt;/a&gt;. Nothing special - pretty noisy.&lt;/p&gt;
&lt;h2 id="day-2-running-and-waterfalls"&gt;Day 2: Running and Waterfalls&lt;/h2&gt;
&lt;p&gt;The marathon training didn’t necessarily stop. We took the early morning and decided to go for a long run along the woods near the Geyser region. We ended up getting lost and running 17km in the middle of the Lunar landscape. Beautiful.&lt;/p&gt;
&lt;p&gt;The second part of the day was all about waterfalls. We drove to the final element of the Golden Triangle: the &lt;a href="https://maps.app.goo.gl/o8j1sqfVBtxPad747"&gt;Gullfoss waterfall&lt;/a&gt;. Once done, we started making our way east while listening to a great &lt;a href="https://podcasts.apple.com/us/podcast/the-uniqueness-of-iceland/id1528861092?i=1000657271696"&gt;podcast episode&lt;/a&gt; about Icelandic Geology.&lt;/p&gt;
&lt;p&gt;On the road to &lt;a href="https://maps.app.goo.gl/oDSkGTL9hgBUq6wd9"&gt;Vik&lt;/a&gt;, two wonders: &lt;a href="https://www.google.com/maps/place/Seljalandsfoss/@63.6114097,-19.988622,13.49z/data=!4m6!3m5!1s0x48d71eade8ef2415:0xae01e6205209178d!8m2!3d63.6156232!4d-19.9885688!16zL20vMDMyZ2tq?entry=ttu&amp;amp;g_ep=EgoyMDI0MDgyMy4wIKXMDSoASAFQAw=="&gt;Seljalandsfoss&lt;/a&gt; is quite a sight. You can go around it too! &lt;a href="https://maps.app.goo.gl/tshJTU2MwUaj75Sc6"&gt;Hestavaðsfoss&lt;/a&gt; was also impressive. Even though our legs were a bit cooked, we hiked a bit as well. Next time, we’ll do it a bit more in this region.&lt;/p&gt;
&lt;p&gt;Tired, we finally reached the &lt;a href="https://maps.app.goo.gl/AzKHjnnYykrs34F1A"&gt;campsite&lt;/a&gt; in Vik. Some well deserved showers, some subpar tortellini and called it a night. Woke up in the middle of the night to a shaking van. Strong winds in the north.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/69/waterfalls_optim.png" alt="Icelandic Landscape" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;h2 id="day-3-a-beach-full-of-ice"&gt;Day 3: A beach full of ice&lt;/h2&gt;
&lt;p&gt;We woke up early to the sound of the wind. After some breakfast and packing, we drove along the east towards the &lt;a href="https://maps.app.goo.gl/XjzPXRTZRNP5fy4DA"&gt;Svínafellsjökull Glacier&lt;/a&gt;. A beautiful location where you park the van, walk ~4/5 km and see the beginning of where the glacier comes down from the mountain.&lt;/p&gt;
&lt;p&gt;After another 45 min drive and we finally ended up at one of the highlights of the trip: &lt;a href="https://maps.app.goo.gl/tUfjSYeX2vnzGXFbA"&gt;Diamond Beach&lt;/a&gt;. It’s a fantastic scenery. It sits just close to there the Jökulsárlón lake touches the Ocean. The glacier melts into the lake, and the large blocks of ice are washed up into the Ocean, some of them end up in a beautiful black sanded beach, melting away. &lt;a href="/photos/f7bb8254f387803f717e339dde92f0e191a023f9.html"&gt;It’s hard to put into words&lt;/a&gt;, but it’s quite the sight.&lt;/p&gt;
&lt;p&gt;Recklessly ignoring the weather warnings, we then drove along the Eastern coast. It was a scary drive, with &lt;em&gt;very strong&lt;/em&gt; winds; between the mountain and the ocean - on a narrow road. Had we gotten a larger van, we would’ve probably stopped right then and there.&lt;/p&gt;
&lt;p&gt;But with Vitto’s co-piloting, we managed to get into the &lt;a href="https://maps.app.goo.gl/2xNgDRZZ4FXLM9G96"&gt;Fossárdalur Campsite&lt;/a&gt;; probably the most beautiful campsite we stayed at. Hidden amongst the Fjords, sheltered from the wind. The kitchen area was super cozy, with electricity, many stoves, and some long tables to enjoy a warm meal.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/69/whales_optim.png" alt="Icelandic Landscape" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;h2 id="day-4-volcanos-and-thermal-baths"&gt;Day 4: Volcanos and Thermal baths&lt;/h2&gt;
&lt;p&gt;We started heading north-west. It’s incredible how the landscape changes while you’re driving in Iceland. It’s truly a moonlike place. On our way to the Mývatn region, we stopped for in one of the strongest waterfalls in the world: the &lt;a href="https://maps.app.goo.gl/GRY1depqdGGkdpko9"&gt;Dettifoss&lt;/a&gt; waterfall. You would’ve never guessed there’s a waterfall there! Especially that large. A bit of walking in a moonlike landscape and BAM! A gigantic waterfall.&lt;/p&gt;
&lt;p&gt;A couple more &lt;a href="https://serialpodcast.org/season-one"&gt;Serial&lt;/a&gt; episodes and driving, and we finally reached the Mývatn lake region. The landscape looks like nothing we’ve ever seen before, it’s just like a Volcano erupted near a lake, and transformed its landscape in a Halong-bay like way.&lt;/p&gt;
&lt;p&gt;During the afternoon, I challenged Vitto to a long run towards a volcano we spotted. At first, we thought we would just get near it, but once we got close enough, it was obvious some people were actually hiking to the top! We had to do the same. The view wasn’t mind-blowing, but the challenge was fun. There’s nothing quite like running as a couple.&lt;/p&gt;
&lt;p&gt;After that long run/trail we were both pretty tired again. We set our GPS to the &lt;a href="https://maps.app.goo.gl/j5gT9iTg6rbYfY7h8"&gt;Mývatn Nature Baths&lt;/a&gt;. If you’ve heard of the &lt;a href="https://www.bluelagoon.com/"&gt;Blue Lagoon&lt;/a&gt;, it’s similar - but a little less packed and touristy. Oh, and it was open until 22:00.&lt;/p&gt;
&lt;p&gt;Once we had enough of the blue water, jacuzzi like temperature, and volcanic views, we decided to call it a night. We got to &lt;a href="https://maps.app.goo.gl/4BHGV3BZi9LL2SFn9"&gt;Camping Myvatn&lt;/a&gt;, another scenic campsite overlooking the lake. A little Barilla and pesto to fill up the belly, and we called it a night.&lt;/p&gt;
&lt;h2 id="day-5-whales-and-fjords"&gt;Day 5: Whales and Fjords&lt;/h2&gt;
&lt;p&gt;When I was young, I was pretty lucky to see the mythical &lt;a href="https://en.wikipedia.org/wiki/Blue_whale"&gt;Blue Whales&lt;/a&gt; in Açores. But Vitto had never seen any! We were told by many that Húsavík was &lt;em&gt;the&lt;/em&gt; capital of whale watching. But we decided we were going to try our luck in the less known &lt;a href="https://www.whalewatchingakureyri.is/"&gt;Akureyri&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We woke up early, packed up our stuff, and drove all the way to Akureyri (to catch the 10:00 AM tour). We had a great time, and saw at least 3/4 whales in different occasions. The tour was well organized, and the staff was calm and professional. The weather was also great, and the whales were out.&lt;/p&gt;
&lt;p&gt;From Akureyri we did another long stretch of driving all the way to the North West region of Iceland. Supposedly less known and more remote. The roads were in less good shape with lots of gravel, but the van handled it.&lt;/p&gt;
&lt;p&gt;Our goal was to reach the &lt;a href="https://maps.app.goo.gl/7zA6fRGYWuTWZXbB9"&gt;Flókalundur&lt;/a&gt; region. The views and road were absolutely mind-blowing. The MAJESTIC Fjords mixed with a Lisbon-like sunlight, is something I have never seen before.&lt;/p&gt;
&lt;p&gt;Just before arriving to camp, we also discovered the little &lt;a href="https://maps.app.goo.gl/aGjwFPPnm1iQsvPy9"&gt;Hellalaugur hot spring&lt;/a&gt;. Just like a little stone jacuzzi in the middle of the rocks. “What language are you guys speaking?” - Asked a German family. “Oh. We speak a mix of Portuguese and Italian”; Always fun looking at their reaction. “We got some of the Italian but couldn’t decrypt the &lt;em&gt;other one&lt;/em&gt;”.&lt;/p&gt;
&lt;p&gt;We decided to indulge ourselves and eat at the &lt;a href="https://maps.app.goo.gl/6Dz67Xq3N1TFpLmM9"&gt;Hotel/Campsite/Restaurant&lt;/a&gt;.  The food was good and reasonably priced! The views from the van were stunning.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/69/beach_van_optim.png" alt="Icelandic Landscape" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;h2 id="day-6-famous-waterfalls-and-road-to-reykjavik"&gt;Day 6: Famous waterfalls and road to Reykjavík&lt;/h2&gt;
&lt;p&gt;We woke up in the middle of the Fjords and set out towards the Dynjandi falls. Arrived early but was already packed. Still, the &lt;a href="https://maps.app.goo.gl/9PMwVq1uqFCdnwG57"&gt;Hæstahjallafoss&lt;/a&gt; falls were quite a site. For a moment, it felt like we were inside an aquarium.&lt;/p&gt;
&lt;p&gt;Kept driving along the coast, and reached the &lt;a href="https://maps.app.goo.gl/juWgKudG6PAHHcfG9"&gt;Kirkjufellsfossar waterfall&lt;/a&gt;. It’s a nice little waterfall that faces a funny shaped mountain called &lt;a href="https://maps.app.goo.gl/D92W4E1sbjgW5bPRA"&gt;Hálsaból Sumarhús&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And we kept driving. We drove most of the day actually. Even though we wanted to get close to Reykjavík, we didn’t want to go back just yet.&lt;/p&gt;
&lt;p&gt;Somehow, probably through some Scandinavian friends, Vitto got a recommendation to hit the &lt;a href="https://maps.app.goo.gl/KVGhW8nyvfmQEEAz7"&gt;Hammsvik Hot Springs&lt;/a&gt;. These were even better than the previous ones we had visited. The weather was cloudy, but with the 4/5 pools near the Fjords’ bay, the scenery was &lt;em&gt;also&lt;/em&gt; magical. The place closed at 22:00, and we left just before 21:55.&lt;/p&gt;
&lt;p&gt;Vitto managed to find a small little camping farm called &lt;a href="https://maps.app.goo.gl/mJ9vjQTZsyRaK2Wz8"&gt;Hjalli Kjós&lt;/a&gt;, we had no big expectations for, but ended up being a nice surprise. Managed by a friendly elderly couple. Clean, cosy, shoes off in common areas. The perfect place to take a shower, cook the last portion of Barilla and hit the bunk for the night.&lt;/p&gt;
&lt;h2 id="day-7-going-back-home"&gt;Day 7: Going back home&lt;/h2&gt;
&lt;p&gt;The last day was pretty uneventful. We woke up early again, took a shower, and drove down to Reykjavík. We dropped the van at &lt;a href="https://maps.app.goo.gl/DXe8fNc5PpSKXSUr9"&gt;KuKu&lt;/a&gt;, made sure everything was fine with it, and were dropped off at the Airport to fly home.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/69/routard.jpeg" alt="Icelandic Landscape" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;h2 id="what-well-do-differently-in-the-next-trip"&gt;What we’ll do differently in the next trip&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;4x4 Camping Van&lt;/strong&gt;. Our van was great. Spacious and well-equipped. We were two, and it could easily sleep 3. However, it was a bit too large and “urban”, and made some of the gravel roads a bit uncomfortable. We didn’t have much choice when booking. But next time, I would probably go with something like &lt;a href="https://happycampers.is/camper/happy-4x4-3-pax/"&gt;this&lt;/a&gt; instead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Less Driving&lt;/strong&gt;: We drove a lot. We wanted to do the complete tour of Iceland. But there were a couple of days when we drove 8+ hours. Even though we listen to nice podcasts, and had nice and long talks, it was a bit too much for our taste. Hey - there are no perfect trips.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More time&lt;/strong&gt;: We had a full 7 days in Iceland. But looking back, I wish we had just a bit more time. I think 8-9 days would be the ideal. Yeah, sure, you could be a month in Iceland. I’m talking about the type of trip &lt;em&gt;we&lt;/em&gt; like to do.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Less Running, More Hiking&lt;/strong&gt;: Our trip was in the middle of summer, in a week in August. But we were also training for an upcoming marathon. So we had to squeeze in runs here in there. That was fun, and we did a lot of those together. But perhaps we would’ve enjoyed even more if we replaced some of those runs for hikes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Vitto was right. When the summer hits, we, southern Europeans, have this tendency to go South. But there was something special about going North this time. As she said: 9 out of 10.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Tanda Runner: A personalized running dashboard</title><link href="https://duarteocarmo.com/blog/tanda-runner.html" rel="alternate"/><published>2024-07-22T17:55:00+02:00</published><updated>2024-07-22T17:55:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-07-22:/blog/tanda-runner.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://tandarunner.duarteocarmo.com"&gt;
&lt;img src="https://duarteocarmo.com/images/68/app.png" alt="Tanda Runner Screenshot"
style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;&lt;a href="https://tandarunner.duarteocarmo.com/"&gt;Tanda Runner&lt;/a&gt; is a web app that shows me the things I care most about when preparing my next marathon. I've also added a running coach/agent designed to give me actionable feedback about my training. Some of that feedback is &lt;em&gt;probably&lt;/em&gt; hallucinatory - I'll get to it in a bit …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://tandarunner.duarteocarmo.com"&gt;
&lt;img src="https://duarteocarmo.com/images/68/app.png" alt="Tanda Runner Screenshot"
style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;&lt;a href="https://tandarunner.duarteocarmo.com/"&gt;Tanda Runner&lt;/a&gt; is a web app that shows me the things I care most about when preparing my next marathon. I've also added a running coach/agent designed to give me actionable feedback about my training. Some of that feedback is &lt;em&gt;probably&lt;/em&gt; hallucinatory - I'll get to it in a bit.&lt;/p&gt;
&lt;p&gt;It's built with &lt;a href="https://www.djangoproject.com/"&gt;Django&lt;/a&gt; - a framework I've gravitated to more and more when building these types of apps. It has all the batteries I need and a time-tested &lt;a href="https://djangopackages.org/grids/g/for-comparison/"&gt;ecosystem&lt;/a&gt;. The front-end is a Frankenstein that uses Django &lt;a href="https://channels.readthedocs.io/en/latest/"&gt;channels&lt;/a&gt;, &lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt;, and some vanilla JavaScript. It’s not the most responsive PWA out there - but it's still a pretty usable experience. The chat interface is adapted from &lt;a href="https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md"&gt;LLaMA.cpp&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The most interesting part is the small LLM coach/agent that gives feedback and tips regarding my performance. It's an &lt;em&gt;untested&lt;/em&gt; and &lt;em&gt;non-evaluated&lt;/em&gt; multi step LLM pipeline that extracts running-related insights from around the web, and uses them to give me personalized feedback using my &lt;a href="https://www.strava.com"&gt;Strava&lt;/a&gt; data.&lt;/p&gt;
&lt;p&gt;To do this, I extract transcripts from marathon related YouTube videos (which I probably &lt;a href="https://www.youtube.com/watch?v=xiJMjTnlxg4"&gt;shouldn't&lt;/a&gt;), and use those to generate a list of insights related to running and training. Once I have those insights, I use an LLM to translate those them into Pandas code. Once that's done I can feed an LLM the insight, the code, and the outcome of running such code on my own data. Sounds confusing? I know it does - but it works surprisingly well!&lt;/p&gt;
&lt;p&gt;This pipeline could probably be significantly improved by designing some evaluations, some metrics, and continuously improving it. But for now, I'll use it to prepare &lt;a href="https://www.marathon06.com/2024/AN/"&gt;Nice&lt;/a&gt;, and make any adjustments I see fit.&lt;/p&gt;
&lt;p&gt;The visualizations and graphs are built using &lt;a href="https://altair-viz.github.io/"&gt;Altair&lt;/a&gt;. They are a mix of my favorite graphs from &lt;a href="https://crplot.com/"&gt;Christoph’s CR Plots&lt;/a&gt;, Strava, and my own brain. The name (and most of the visualizations) come from &lt;a href="https://scholar.google.co.uk/citations?view_op=view_citation&amp;amp;hl=en&amp;amp;user=C__krSUAAAAJ&amp;amp;cstart=20&amp;amp;pagesize=80&amp;amp;citation_for_view=C__krSUAAAAJ:j3f4tGmQtD8C"&gt;Giovanni Tanda&lt;/a&gt;'s work - which I encourage you to read through!&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Do things that don't scale</title><link href="https://duarteocarmo.com/blog/do-things-that-dont-scale.html" rel="alternate"/><published>2024-06-06T10:00:00+02:00</published><updated>2024-06-06T10:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-06-06:/blog/do-things-that-dont-scale.html</id><summary type="html">&lt;p&gt;I'm lucky enough to work with some pretty talented folks. During a recent offsite, one of them gave a completely improvised talk. He had just gotten back home from holidays - so naturally, they didn't really have time to prepare anything. But what they said resonated.&lt;/p&gt;
&lt;p&gt;It was about &lt;em&gt;improvisation&lt;/em&gt;. How …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I'm lucky enough to work with some pretty talented folks. During a recent offsite, one of them gave a completely improvised talk. He had just gotten back home from holidays - so naturally, they didn't really have time to prepare anything. But what they said resonated.&lt;/p&gt;
&lt;p&gt;It was about &lt;em&gt;improvisation&lt;/em&gt;. How a lot of things we do every day are - to some extent, made up! From how we greet each other, to how we respond to an unexpected phone call. None of it is planned. We don't create some sort of crazy scalable distributed architecture to deal with these things: They're &lt;em&gt;&lt;a href="https://en.wikipedia.org/wiki/Lick_(music)"&gt;licks&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;For some, this felt counter-intuitive. For me, it really hit home.&lt;/p&gt;
&lt;p&gt;But I understand how it might feel counter-intuitive. Experienced engineers think &lt;em&gt;hard&lt;/em&gt; before solving problems - they're expected to. "Hey - we've seen this before..." -  They'll research. They'll anticipate problems. Even when the problem is not well-defined, they'll work hard on defining it! It doesn't even cross our minds that a badly defined problem is probably not a problem to begin with. With all this thinking, they enter &lt;em&gt;the ether&lt;/em&gt;. The ether is that stale anticipation, that hesitation. It takes many forms - but in the engineering world it's normally in the form of endless research, stalling, no decisions being made. The ether is this "pre-problem" hesitation.&lt;/p&gt;
&lt;p&gt;The talk also argued about how perfect is &lt;a href="https://www.youtube.com/watch?v=CZ8fTfpyqpQ&amp;amp;t=211s"&gt;the ENEMY&lt;/a&gt; of good enough. While we're thinking of the perfect solution, the original problem is &lt;em&gt;still&lt;/em&gt; there. Users don't see your research, users don't see the architecture meetings, users don't see &lt;em&gt;the ether&lt;/em&gt;. The only thing they see is the problem. And guess what? The problem is still there.&lt;/p&gt;
&lt;p&gt;And yeah, good enough is likely pretty bad. The first solution, will probably suck. It was fast, we were in a rush. We just wanted to put something out there to solve a problem! And obviously, we didn't think of all the ways it would/could break.&lt;/p&gt;
&lt;p&gt;It's a privilege to build something that doesn't scale. We might argue that if you get stuck in the ether in the first place, you won't even get to scale being problem. Scale is a great problem to have. It means your solution &lt;em&gt;started&lt;/em&gt; solving a problem - and people want more of it. This is a good thing.&lt;/p&gt;
&lt;p&gt;And yes, now we have &lt;em&gt;double the trouble&lt;/em&gt;. Now we need to scale AND handle users at the same time. But what's the alternative? Getting stuck in the ether and potentially building something we don't even know if people will use?&lt;/p&gt;
&lt;p&gt;I like to think that doing things that don't scale will likely save time in the long run. Lots of things we build don't get used. And so if you spend a lot of time in the ether - you risk that time being completely wasted. And there are no guarantees you'll likely be able to anticipate all the ways you won't scale. But getting something out there, in front of people, takes you from 0 to 1. And the faster we realize what we've built doesn't solve anyone's problem, the better.&lt;/p&gt;
&lt;p&gt;And solving problems is what we're here for.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>How I self-host in 2024</title><link href="https://duarteocarmo.com/blog/how-i-self-host-in-2024.html" rel="alternate"/><published>2024-04-28T19:00:00+02:00</published><updated>2024-04-28T19:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-04-28:/blog/how-i-self-host-in-2024.html</id><summary type="html">&lt;p&gt;I'm a sucker for side projects. There's nothing quite like building something to learn about it.&lt;/p&gt;
&lt;p&gt;Over the last 5+ years I've accumulated a little over 15 small web apps and websites. Almost all of them are hosted on a small Hetzner server and deployed using &lt;a href="https://duarteocarmo.com/blog/down-from-the-cloud-self-hosting.html"&gt;a mix of ssh …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;I'm a sucker for side projects. There's nothing quite like building something to learn about it.&lt;/p&gt;
&lt;p&gt;Over the last 5+ years I've accumulated a little over 15 small web apps and websites. Almost all of them are hosted on a small Hetzner server and deployed using &lt;a href="https://duarteocarmo.com/blog/down-from-the-cloud-self-hosting.html"&gt;a mix of ssh, docker-compose, and Caddy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Some months ago my small server started giving me some problems. The 4 vCPUs and 16 GB RAM weren't enough, and hiccups became more frequent. I also want to run more and more small LLMs on it, which turned out to be &lt;em&gt;&lt;a href="https://github.com/duarteocarmo/lusiaidas/blob/master/app.py#L1"&gt;challenging&lt;/a&gt;&lt;/em&gt;. The whole &lt;a href="https://github.com/duarteocarmo/governosombra/blob/master/.github/workflows/workflow.yml"&gt;"ssh with root using github actions"&lt;/a&gt; started to look like more of a limitation than a feature.&lt;/p&gt;
&lt;p&gt;It was time for an upgrade.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/66/coolify-apps.png" alt="Coolify apps"
style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;First, I needed to upgrade my hardware. Not sure if &lt;a href="https://www.hetzner.com/"&gt;Hetzner&lt;/a&gt; is the cheapest out there, but after using them for 5+ years I can definitely say they're reliable. I went with their &lt;a href="https://www.hetzner.com/sb"&gt;server auction&lt;/a&gt;, which is a great (and sustainable) way of getting a nice refurbished server for a fair price. For about 30 EUR/month, I upgraded to a nice Intel i7 with 64GB RAM(!). Should be more than enough.&lt;/p&gt;
&lt;p&gt;With better hardware, I needed to decide whether I would keep my &lt;a href="https://duarteocarmo.com/blog/down-from-the-cloud-self-hosting.html"&gt;old setup&lt;/a&gt;, or upgrade to something a bit more &lt;em&gt;streamlined&lt;/em&gt; (god I &lt;em&gt;hate&lt;/em&gt; that word). Turns out, there are quite some open source PaaS alternatives. From &lt;a href="https://dokku.com/"&gt;Dokku&lt;/a&gt;, &lt;a href="https://caprover.com/"&gt;CapRover&lt;/a&gt;, to DHH's &lt;a href="https://kamal-deploy.org/"&gt;Kamal&lt;/a&gt;. The one who caught my eye the most was &lt;a href="https://coolify.io/"&gt;Coolify&lt;/a&gt;. Without much due diligence, it seemed fit most of what I needed: &lt;a href="https://github.com/coollabsio/coolify"&gt;Open source&lt;/a&gt;, &lt;a href="https://coolify.io/docs/installation"&gt;easy to install&lt;/a&gt;, &lt;a href="https://github.com/coollabsio/coolify/commits/main/"&gt;active project&lt;/a&gt;, &lt;a href="https://coolify.io/docs/screenshots"&gt;with a polished interface&lt;/a&gt;, &lt;a href="https://coolify.io/docs/knowledge-base/git/github/integration"&gt;integrates&lt;/a&gt; with GitHub, and supports docker-based deployments. That's not an easy list to check-off.&lt;/p&gt;
&lt;p&gt;The migration was pretty smooth, except for &lt;a href="https://aicoverlettercreator.com/"&gt;a&lt;/a&gt; &lt;a href="https://infrequent.app/"&gt;couple&lt;/a&gt; of more complicated Django applications with databases on docker volumes. Since everything was already running on containers, it was pretty much plug-and-play. In a couple of days everything was migrated.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/66/kuma-screenshot.png" alt="Kuma screenshot"
style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;There are a lot of things I love about Coolify. It scans my GitHub repos and redeploys when needed via webhooks. It also sends me Telegram notifications when something gets updated to a new version. Finally, it also provides a wide range of other applications you can deploy (Databases, S3-compatible storages, and a &lt;a href="https://coolify.io/docs/resources/services/index"&gt;wide range&lt;/a&gt; of other services).&lt;/p&gt;
&lt;p&gt;Don't get me wrong - I love the Cloud. But essentially, the Cloud is just someone else’s computer. So why shouldn’t it be my computer? Self-hosting everything means I can pay a fixed price for my hardware, and &lt;em&gt;not&lt;/em&gt; for the number of applications that run on it. And even though I did like my little &lt;em&gt;hacked-together&lt;/em&gt; setup, I love being able to use, AND &lt;a href="https://github.com/coollabsio/coolify/pull/2028"&gt;contribute&lt;/a&gt; to a promising open source project!&lt;/p&gt;
&lt;p&gt;There's something incredible about what a &lt;a href="https://github.com/andrasbacsai"&gt;1-man team&lt;/a&gt; can accomplish with the support of the open source community.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>The best keyboard I've ever had</title><link href="https://duarteocarmo.com/blog/happy-hacking-keyboard-review.html" rel="alternate"/><published>2024-04-22T21:30:00+02:00</published><updated>2024-04-22T21:30:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-04-22:/blog/happy-hacking-keyboard-review.html</id><summary type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/65/top.png" alt="Top view hhkb" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;There I was. Ready to click the big blue purchase button. &lt;a href="https://www.amazon.de/-/en/Keyboard-PD-KB800WS-Professional-Mechanical-Bluetooth/dp/B082TQK2SB/ref=sr_1_5?crid=252LFZZHYLWDD&amp;amp;dib=eyJ2IjoiMSJ9.gyKtvGmLTXpI3swZKvqN1nA1_IpxN0tDr08V-ZBKHBmG1lD2gKTlEwNwkO4E9kuJXwi1xyyVHfl40hOiT_yN-pl5u__XxzMT5NhETtf5gDxP7fNbDTKpQfLR6yWiT_xAOII9MF-sFnUNT0skIHa6mX3_S9bqgac_NuKq2Os7PgVtEDF0dZ-JfTAtB281d0B9SfhQ_IbJ7wE8rjYmRHT8_Txvk7KcJKX2GvjIqiQnCrM.QEr4ATZDMt12kc9i0f5UwxTYd3RtKaMZSs8CND32-qs&amp;amp;dib_tag=se&amp;amp;keywords=happy+hacking+keyboard+hybrid+s&amp;amp;qid=1713811000&amp;amp;sprefix=happy+hacking%2Caps%2C90&amp;amp;sr=8-5&amp;amp;ufe=app_do%3Aamzn1.fos.335e368b-29e8-4542-bb58-939a88195e78"&gt;350 Euro&lt;/a&gt;, for a &lt;em&gt;keyboard&lt;/em&gt; (!) Asking Vitto multiple times: is this &lt;em&gt;really&lt;/em&gt; worth it? Should I go for it? I mean, that's a whole lot of dinners.&lt;/p&gt;
&lt;p&gt;After much hesitation, I did it. &lt;em&gt;All black&lt;/em&gt;, with blank key caps …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/65/top.png" alt="Top view hhkb" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;There I was. Ready to click the big blue purchase button. &lt;a href="https://www.amazon.de/-/en/Keyboard-PD-KB800WS-Professional-Mechanical-Bluetooth/dp/B082TQK2SB/ref=sr_1_5?crid=252LFZZHYLWDD&amp;amp;dib=eyJ2IjoiMSJ9.gyKtvGmLTXpI3swZKvqN1nA1_IpxN0tDr08V-ZBKHBmG1lD2gKTlEwNwkO4E9kuJXwi1xyyVHfl40hOiT_yN-pl5u__XxzMT5NhETtf5gDxP7fNbDTKpQfLR6yWiT_xAOII9MF-sFnUNT0skIHa6mX3_S9bqgac_NuKq2Os7PgVtEDF0dZ-JfTAtB281d0B9SfhQ_IbJ7wE8rjYmRHT8_Txvk7KcJKX2GvjIqiQnCrM.QEr4ATZDMt12kc9i0f5UwxTYd3RtKaMZSs8CND32-qs&amp;amp;dib_tag=se&amp;amp;keywords=happy+hacking+keyboard+hybrid+s&amp;amp;qid=1713811000&amp;amp;sprefix=happy+hacking%2Caps%2C90&amp;amp;sr=8-5&amp;amp;ufe=app_do%3Aamzn1.fos.335e368b-29e8-4542-bb58-939a88195e78"&gt;350 Euro&lt;/a&gt;, for a &lt;em&gt;keyboard&lt;/em&gt; (!) Asking Vitto multiple times: is this &lt;em&gt;really&lt;/em&gt; worth it? Should I go for it? I mean, that's a whole lot of dinners.&lt;/p&gt;
&lt;p&gt;After much hesitation, I did it. &lt;em&gt;All black&lt;/em&gt;, with blank key caps. Yes, I too was skeptical from the start. The first days, I couldn't type 4 keys in a row without having to pull up the manual. 2 days passed, then 3 days passed... Still, I didn't get it. How do people use this keyboard? How am I supposed to type a full credit card number with blank key caps? You're a pro touch typist? Good for you. I'm not.&lt;/p&gt;
&lt;p&gt;"I can get used to this" I would tell myself. But after a week using the keyboard, I've had enough. Basta. This isn't going to cut it for me. I'm just too slow with it. 350 Euro.. I'm not only poorer but now I'm also &lt;em&gt;slower&lt;/em&gt; at typing.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/65/return.png" alt="Returning the HHKB keyboard" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;I decided to return the thing&lt;sup id="sf-happy-hacking-keyboard-review-1-back"&gt;&lt;a href="#sf-happy-hacking-keyboard-review-1" class="simple-footnote" title="Don't buy from PFU EMEA, order from Amazon directly."&gt;1&lt;/a&gt;&lt;/sup&gt;. And went back to my good ol' &lt;a href="https://www.keychron.com/collections/normal-profile-keyboards/products/keychron-k2-hot-swappable-wireless-mechanical-keyboard"&gt;Keychron K2&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="a-second-chance"&gt;A second chance&lt;/h2&gt;
&lt;p&gt;Some days passed and something was just not right. This can't be! I can't be missing that piece of junk. There's just something about those keys.. Typing on that keyboard just felt &lt;em&gt;right&lt;/em&gt;. Even if I didn't know where each key was.. I just feels good!&lt;/p&gt;
&lt;p&gt;I reordered it. And we were not about to make the same 'blank key caps' mistake. I ordered the white version with printed key caps. Just to make sure I CAN SEE EVERYTHING.&lt;/p&gt;
&lt;h2 id="some-cons"&gt;Some cons&lt;/h2&gt;
&lt;p&gt;It's perfect, but there are still some things I'm not in love with. For 350 EUR I expect everything to be absolutely perfect. I mean, &lt;em&gt;really&lt;/em&gt; perfect. The materials are good, but for the most, they are still plastic. Every time I want to start using the keyboard, I need to click and hold the little power button in the back.&lt;/p&gt;
&lt;p&gt;Another small complaint is the layout. This keyboard changes the place of the Control key, removes the delete key, has no dedicated arrow keys. For the most part, it works great (I use Vim keybindings for most things). But now my brain goes through a small hiccup every time I use a 'normal' keyboard. And my pinky still hurts. Maybe my hands are just big, I don't know.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/65/setup.png" alt="Setup view happy hacking keyboard" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;h2 id="what-made-me-fall-in-love"&gt;What made me fall in love&lt;/h2&gt;
&lt;p&gt;It was the typing experience. It’s really like typing on a cloud. I'm not a big mechanical keyboard head. I've had a couple of &lt;a href="https://www.keychron.com/"&gt;Keychron&lt;/a&gt; keyboards, that's it. But these &lt;a href="https://deskthority.net/wiki/Topre_switch"&gt;Topre switches&lt;/a&gt; are just something else. They are smooth and squishy at the same time. It feels as though your fingers are just swimming in the keyboard. &lt;em&gt;That's&lt;/em&gt; what got me hooked from the start.&lt;/p&gt;
&lt;p&gt;There are two other things I love about the &lt;a href="https://www.amazon.de/-/en/Keyboard-PD-KB800WS-Professional-Mechanical-Bluetooth/dp/B082TQK2SB/ref=sr_1_5?crid=252LFZZHYLWDD&amp;amp;dib=eyJ2IjoiMSJ9.gyKtvGmLTXpI3swZKvqN1nA1_IpxN0tDr08V-ZBKHBmG1lD2gKTlEwNwkO4E9kuJXwi1xyyVHfl40hOiT_yN-pl5u__XxzMT5NhETtf5gDxP7fNbDTKpQfLR6yWiT_xAOII9MF-sFnUNT0skIHa6mX3_S9bqgac_NuKq2Os7PgVtEDF0dZ-JfTAtB281d0B9SfhQ_IbJ7wE8rjYmRHT8_Txvk7KcJKX2GvjIqiQnCrM.QEr4ATZDMt12kc9i0f5UwxTYd3RtKaMZSs8CND32-qs&amp;amp;dib_tag=se&amp;amp;keywords=happy+hacking+keyboard+hybrid+s&amp;amp;qid=1713811000&amp;amp;sprefix=happy+hacking%2Caps%2C90&amp;amp;sr=8-5&amp;amp;ufe=app_do%3Aamzn1.fos.335e368b-29e8-4542-bb58-939a88195e78"&gt;Happy Hacking Keyboard&lt;/a&gt;. First, is the battery. I've had this keyboard for over a month, and did not think about battery a single time. I’ve come to realize just how much I hate charging my keyboard every week. I prefer to use a couple of AA batteries every couple of months.&lt;/p&gt;
&lt;p&gt;The second thing is the sound. I like mechanical - but I don't enjoy everyone around the house hearing the hammering sound of plastic against plastic, or Vitto having to know every time I write to file in Neovim. I enjoy working around people sometimes. With the HHKB I can do so without bothering everyone around me.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;So yeah. &lt;a href="https://gizmodo.com/this-is-the-perfect-keyboard-1845258727"&gt;I have to agree with Alex&lt;/a&gt;. This might very well be the perfect keyboard. I'm not 100% sold on all the choices that were made, but I can see &lt;em&gt;why&lt;/em&gt; they were made. And the typing experience got me hooked from the first day (or at least once I knew where all the keys were).&lt;/p&gt;
&lt;p&gt;There is only one last thing that will truly tell if this is the perfect keyboard: Time. And even though I have good trust on &lt;a href="https://en.wikipedia.org/wiki/Happy_Hacking_Keyboard"&gt;Japanese manufacturing&lt;/a&gt;, we'll only know the result of that test in a decade.&lt;/p&gt;
&lt;hr&gt;&lt;ol class="simple-footnotes"&gt;&lt;li id="sf-happy-hacking-keyboard-review-1"&gt;Don't buy from &lt;a href="https://www.pfuemea.com/en-gb"&gt;PFU EMEA&lt;/a&gt;, order from Amazon directly. &lt;a href="#sf-happy-hacking-keyboard-review-1-back" class="simple-footnote-back"&gt;↩︎&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;</content><category term="blog"/></entry><entry><title>An experiment with Gemma 2B and a Portuguese classic</title><link href="https://lusiaidas.duarteocarmo.com/" rel="alternate"/><published>2024-04-10T20:00:00+02:00</published><updated>2024-04-10T20:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:lusiaidas.duarteocarmo.com,2024-04-10:/</id><content type="html"/><category term="blog"/></entry><entry><title>NewsHavn: Danish news, in English</title><link href="https://duarteocarmo.com/blog/newshavn-danish-news-in-english.html" rel="alternate"/><published>2024-02-26T20:30:00+01:00</published><updated>2024-02-26T20:30:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-02-26:/blog/newshavn-danish-news-in-english.html</id><summary type="html">&lt;p&gt;&lt;em&gt;Conversas de café&lt;/em&gt;. Literally translated, means "coffee shop conversations".&lt;/p&gt;
&lt;p&gt;An upcoming election, the weather next week, a corruption scandal, a new policy. Just some examples of &lt;em&gt;Conversas de café&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;As an expat living in Denmark for the past 7 years (without speaking the language), that's perhaps one of the things …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;Conversas de café&lt;/em&gt;. Literally translated, means "coffee shop conversations".&lt;/p&gt;
&lt;p&gt;An upcoming election, the weather next week, a corruption scandal, a new policy. Just some examples of &lt;em&gt;Conversas de café&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;As an expat living in Denmark for the past 7 years (without speaking the language), that's perhaps one of the things I miss most. Maybe it's because of where I'm from, but for me, water cooler conversations &lt;em&gt;are&lt;/em&gt; Culture.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Learn the language&lt;/em&gt;, &lt;em&gt;use Google Translate&lt;/em&gt;, &lt;em&gt;read &lt;a href="https://www.thelocal.dk/"&gt;some&lt;/a&gt; expat focused website&lt;/em&gt;. No thanks. I want to read the same stuff people around me read. I want to understand what worries &lt;em&gt;them&lt;/em&gt;, not other expats.&lt;/p&gt;
&lt;p&gt;Enter &lt;a href="https://newshavn.duarteocarmo.com/"&gt;NewsHavn&lt;/a&gt;.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://newshavn.duarteocarmo.com" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/63/website.png" alt="Newshavn.duarteocarmo.com"
style="max-width:100%;border-radius: 2px"&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;Newshavn parses the RSS feeds of a couple of big Danish newspapers and uses &lt;a href="https://github.com/duarteocarmo/NewsHavn/blob/8758d1f8214e2c54c24c06d0c0ba42b92e78c474/parser/parse.go#L173"&gt;Mistral 7B&lt;/a&gt; to translate them. The design is inspired by &lt;a href="https://text.npr.org/"&gt;NPR&lt;/a&gt;. The code is all &lt;a href="https://github.com/duarteocarmo/NewsHavn"&gt;opensource&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For this one, I decided to use Go (again). I must say, Go is growing on me. Rust is fun and efficient, but I feel like Go strikes the right balance between strictness and flexibility. Another thing I love about Go is its concurrency model. &lt;a href="https://gobyexample.com/goroutines"&gt;Goroutines&lt;/a&gt; are just beautiful, and make me want to write more concurrent code. It feels like it's a no-bullshit language, and I'm a fan of no-bullshit.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>mistral-doc: Fine-tuning an LLM on my ChatGPT conversations</title><link href="https://duarteocarmo.com/blog/mistral-doc-fine-tuning-an-llm-on-my-chatgpt-conversations.html" rel="alternate"/><published>2024-02-09T13:00:00+01:00</published><updated>2024-02-09T13:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-02-09:/blog/mistral-doc-fine-tuning-an-llm-on-my-chatgpt-conversations.html</id><summary type="html">&lt;p&gt;&lt;i&gt;&lt;a href="https://github.com/duarteocarmo/mistral-doc" target="_blank"&gt;Step-by-step instructions to do it yourself&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;If you ever read this blog, you probably already know by now. For the past couple of months, I've been trying to create my &lt;em&gt;very own&lt;/em&gt; Large Language Model.&lt;/p&gt;
&lt;p&gt;OpenAI's models have had a significant impact on my productivity. But something still bothers me …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;i&gt;&lt;a href="https://github.com/duarteocarmo/mistral-doc" target="_blank"&gt;Step-by-step instructions to do it yourself&lt;/a&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;If you ever read this blog, you probably already know by now. For the past couple of months, I've been trying to create my &lt;em&gt;very own&lt;/em&gt; Large Language Model.&lt;/p&gt;
&lt;p&gt;OpenAI's models have had a significant impact on my productivity. But something still bothers me. My data, my preferences, my history, my concerns, all going into the hands of a single company.&lt;/p&gt;
&lt;p&gt;But &lt;em&gt;this&lt;/em&gt; time, &lt;em&gt;this&lt;/em&gt; time I think I got really close! The result is &lt;code&gt;mistral-doc&lt;/code&gt;, a Mistral fine-tune on all my ChatGPT conversations.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/62/ollama.png" alt="Ollama SwiftUI with mistral-doc running"
style="max-width:100%;border-radius: 2px"&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;The goal is to emulate my current ChatGPT plus experience. My favorite thing about GPT4, is how obedient it is to &lt;a href="https://github.com/duarteocarmo/mistral-doc/blob/846bff73cdf065620d529b2024e7cfba774dc0a5/Modelfile#L24"&gt;my system prompt&lt;/a&gt;. &lt;em&gt;Especially&lt;/em&gt; when I just want a short and concise answer. &lt;a href="https://chat.openai.com/share/84a98280-ac66-41b4-9158-cbccc149cbdd"&gt;When I ask who was the first king of Portugal&lt;/a&gt;, I want the answer to be short (and sure, correct is also good).&lt;/p&gt;
&lt;p&gt;Over the past year, I've accumulated a little over 1000(!) conversations with ChatGPT. Thankfully, OpenAI actually makes exporting all my data pretty straightforward. So, I built a small &lt;a href="https://github.com/duarteocarmo/mistral-doc/blob/main/process_gpt_export.py"&gt;script&lt;/a&gt; that could process the export, and store all of my conversations as a Hugging Face dataset. It's private, of course, but you can use the same script to build yours.&lt;/p&gt;
&lt;p&gt;There are thousands of models I could potentially fine-tune on my conversations. And the model choice will &lt;em&gt;for sure&lt;/em&gt;, have an impact on the quality of the results. But I decided to go with something small, so that I could train everything in about ~2 hours with &lt;a href="https://github.com/OpenAccess-AI-Collective/axolotl"&gt;Axolotl&lt;/a&gt;. I decided to go with &lt;code&gt;Mistral-7B-Instruct-v0.2&lt;/code&gt;. Yes, a base model would likely learn a bit more. But I don't want to teach a model to chat, I want to teach a model to learn from &lt;em&gt;my&lt;/em&gt; chats.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/62/merged.png" alt="Mistral-doc instruct merged on hugging face"
style="max-width:100%;border-radius: 2px"&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;With Axolotl and a 40GB/RAM GPU machine in runpod, the fine-tuning process took around 2.5 hours. Here's the &lt;a href="https://github.com/duarteocarmo/mistral-doc/blob/main/configs/mistral-doc-instruct.yml"&gt;config&lt;/a&gt; I used by the way. Once the training was done, I had to &lt;em&gt;merge the model back to base&lt;/em&gt; (we trained a &lt;a href="https://magazine.sebastianraschka.com/p/practical-tips-for-finetuning-llms"&gt;Lora adapter&lt;/a&gt;, but want the whole thing). Thankfully, Axolotl also &lt;a href="https://github.com/OpenAccess-AI-Collective/axolotl?tab=readme-ov-file#merge-lora-to-base"&gt;handles this&lt;/a&gt;. In the end of the process, I successfully had my entire ~15 GB model in a repository I called &lt;code&gt;mistral-doc-instruct-v4-merged&lt;/code&gt;. Yes, there were 3 other versions where I screwed things up.&lt;/p&gt;
&lt;p&gt;With the entire fine-tuned model ready, it was now time to run it on my machine. Before I did that, I had to ensure the model was going to be of a reasonable size and speed. I used &lt;a href="https://github.com/ggerganov/llama.cpp"&gt;llama.cpp&lt;/a&gt; to first convert the model to the &lt;code&gt;GGUF&lt;/code&gt; format, and then to &lt;a href="https://sebastianraschka.com/blog/2023/llm-mixed-precision-copy.html#quantization"&gt;quantize&lt;/a&gt; it to 4-bits. This process resulted in a &lt;em&gt;single&lt;/em&gt; file, called &lt;code&gt;mistral-doc.gguf&lt;/code&gt;, which is ~4.5 GB in size.&lt;/p&gt;
&lt;p&gt;Now we can take for a spin! Using &lt;a href="https://ollama.ai/"&gt;Ollama&lt;/a&gt;, I can create a &lt;a href="https://github.com/duarteocarmo/mistral-doc/blob/main/Modelfile"&gt;&lt;code&gt;Modelfile&lt;/code&gt;&lt;/a&gt; with all the details and configuration I need. With that file, I can now create my own model in the Ollama format with:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# create the model with ollama&lt;/span&gt;

$&lt;span class="w"&gt; &lt;/span&gt;ollama&lt;span class="w"&gt; &lt;/span&gt;create&lt;span class="w"&gt; &lt;/span&gt;mistral-doc&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;./Modelfile

&lt;span class="c1"&gt;# chat with your fine tune&lt;/span&gt;

$&lt;span class="w"&gt; &lt;/span&gt;ollama&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;mistral-doc
&amp;gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;vv&lt;span class="w"&gt; &lt;/span&gt;jasmin&lt;span class="w"&gt; &lt;/span&gt;or&lt;span class="w"&gt; &lt;/span&gt;basmati&lt;span class="w"&gt; &lt;/span&gt;rice&lt;span class="w"&gt; &lt;/span&gt;with&lt;span class="w"&gt; &lt;/span&gt;falafel?
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Running the model in the terminal is all fine and dandy, but I want something that can actually replace the instinct of going into a browser and typing &lt;code&gt;chat.openai.com&lt;/code&gt;. I downloaded a small app called &lt;a href="https://github.com/kghandour/Ollama-SwiftUI"&gt;&lt;code&gt;Ollama-SwiftUI&lt;/code&gt;&lt;/a&gt; that lets me run my model in a nice little Mac app. Currently, I'm jumping around between using that, and &lt;a href="https://lmstudio.ai/"&gt;LM Studio&lt;/a&gt; to interact with &lt;code&gt;mistral-doc&lt;/code&gt;.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/62/lmstudio.png" alt="Mistral doc in action in LMStudio"
style="max-width:100%;border-radius: 2px"&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;How has the experience been so far? Well for starters, its much better than any off-the-shelf ~7 billion parameter model I've tried before. For simple and straightforward tasks, it's a great alternative to ChatGPT Plus. I should note that it's not better because it's smarter or does math better. This model is better because it has learned how &lt;em&gt;I&lt;/em&gt; like questions to be answered. This of course does &lt;em&gt;not&lt;/em&gt; mean you will like it, which is why I'm not going to publish it. We could make it even better! I could fine-tune something like &lt;a href="https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1"&gt;Mixtral&lt;/a&gt;, which would surely make it even more powerful! But I would probably not be able to run it on my laptop, and would have to pay someone else to host it. I could also generate even more data, increase the number of epochs, teach it more things about me.&lt;/p&gt;
&lt;p&gt;Not sure I will; but this process definitely looks promising.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>What's in my NOW?</title><link href="https://kk.org/cooltools/whats-in-my-now-duarte-o-carmo/" rel="alternate"/><published>2024-02-01T10:00:00+01:00</published><updated>2024-02-01T10:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:kk.org,2024-02-01:/cooltools/whats-in-my-now-duarte-o-carmo/</id><content type="html"/><category term="blog"/></entry><entry><title>Self-hosting my personal LLM (but not quite)</title><link href="https://duarteocarmo.com/blog/self-hosting-llm-ambrosio.html" rel="alternate"/><published>2024-01-21T22:35:00+01:00</published><updated>2024-01-21T22:35:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-01-21:/blog/self-hosting-llm-ambrosio.html</id><summary type="html">&lt;p&gt;ChatGPT is now part of our daily lives. A quick question, an extra input, some quick feedback, I always reach for it. AGI or not AGI, I certainly can't deny the impact it has had on our lives. It's pretty incredible!&lt;/p&gt;
&lt;p&gt;But that small voice just won't go away. &lt;em&gt;"Where …&lt;/em&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;ChatGPT is now part of our daily lives. A quick question, an extra input, some quick feedback, I always reach for it. AGI or not AGI, I certainly can't deny the impact it has had on our lives. It's pretty incredible!&lt;/p&gt;
&lt;p&gt;But that small voice just won't go away. &lt;em&gt;"Where is all this data going? Should I really be telling this bot this much? What if someone else sees this?"&lt;/em&gt;
In a way, it feels like everything I tell this bot is going into a void I have no control of. I can't be the only one feeling it.&lt;/p&gt;
&lt;p&gt;And I don't like it. It's a bit like putting all my eggs in a single basket. As engineers, we know how bad having a single point of failure is. Resilience! That's what we were taught.&lt;/p&gt;
&lt;p&gt;Given that I already started building &lt;a href="https://github.com/duarteocarmo/ambrosio"&gt;Ambrosio&lt;/a&gt;, so why not use it to self-host my own LLM?&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/60/ambrosio.png" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/60/ambrosio.png" alt="Ambrosio chat and generate photo" style="max-width:95%;border-radius: 2px"&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;h2 id="self-hosting-with-the-help-of-llamacpp"&gt;Self-hosting with the help of Llama.cpp&lt;/h2&gt;
&lt;p&gt;I'm not really interested in spending 500 USD/month to run a model that requires 40 GB of VRAM on a large GPU. So I had to lower my standards a little bit. The premise was now: What models can I run on a 15 USD/month server?&lt;/p&gt;
&lt;p&gt;I started looking at the &lt;a href="https://huggingface.co/spaces/lmsys/chatbot-arena-leaderboard"&gt;only LLM leaderboard I trust&lt;/a&gt;. There aren't many options with 7 billion parameters. The best one seems to be &lt;a href="https://huggingface.co/berkeley-nest/Starling-LM-7B-alpha"&gt;Starling-LM-7B-alpha&lt;/a&gt;, out of Berkeley. &lt;a href="https://huggingface.co/TheBloke/Starling-LM-7B-alpha-GGUF#provided-files"&gt;Quantized&lt;/a&gt;, it should just about run on a cheap Hetzner box. Getting it up and running on a small box with &lt;a href="https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md"&gt;llama.cpp&lt;/a&gt; is as easy as:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# download model&lt;/span&gt;

$&lt;span class="w"&gt; &lt;/span&gt;wget&lt;span class="w"&gt; &lt;/span&gt;https://huggingface.co/TheBloke/Starling-LM-7B-alpha-GGUF/resolve/main/starling-lm-7b-alpha.Q5_K_M.gguf?download&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# setup llama cpp API with ./server&lt;/span&gt;

$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/ggerganov/llama.cpp
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;llama.cpp
$&lt;span class="w"&gt; &lt;/span&gt;make
$&lt;span class="w"&gt; &lt;/span&gt;./server&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;starling-lm-7b-alpha.Q5_K_M.gguf&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8192&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--temp&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt; &lt;/span&gt;--repeat_penalty&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;-1&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GPT4 User: {prompt}&amp;lt;|end_of_turn|&amp;gt;GPT4 Assistant:&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And just like that, you're running your own OpenAI compatible (&lt;em&gt;ugh&lt;/em&gt;, I hate that term) API serving that very 7B model.&lt;/p&gt;
&lt;p&gt;Now, don't get me wrong, I think this model is great! But I'm looking for something that will replace my daily use of ChatGPT. After integrating with &lt;a href="https://github.com/duarteocarmo/ambrosio"&gt;Ambrosio&lt;/a&gt; the limitations were obvious. The first issue: It's slow, painfully slow (which was expected on my small machine). And even though it's pretty incredible for a small model, it's not going to replace even the most basic use cases.&lt;/p&gt;
&lt;p&gt;And if it sounds unfair in any way, it's because it is! How could I expect the same level of quality from something that can run on a cheap VM, to something like ChatGPT? And you're right. We can't. So what else can we do?&lt;/p&gt;
&lt;h2 id="mixtral-8x7b-and-the-rice-test"&gt;Mixtral 8x7B and the rice test&lt;/h2&gt;
&lt;p&gt;Let's say I was going to rent a bigger machine for a better model. What would I run on it? Certainly not a 7B model! Looking at the rankings again, the best open source thing currently is &lt;a href="https://huggingface.co/TheBloke/Mixtral-8x7B-Instruct-v0.1-GGUF"&gt;Mixtral 8x7b&lt;/a&gt;, a mixture-of-experts model from &lt;a href="https://mistral.ai/"&gt;Mistral&lt;/a&gt;. Just glancing at the &lt;a href="https://huggingface.co/TheBloke/Mixtral-8x7B-Instruct-v0.1-GGUF#provided-files"&gt;requirements&lt;/a&gt;, and it was pretty clear this needed to be run on someone else's box. I decided to give &lt;a href="https://www.together.ai"&gt;together.ai&lt;/a&gt; a go. Their pricing is pretty much unbeatable and their inference speed is &lt;em&gt;blazingly&lt;/em&gt; fast. And of course they have an &lt;a href="https://docs.together.ai/docs/openai-api-compatibility"&gt;Open AI compatible API&lt;/a&gt;.&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://duarteocarmo.com/images/60/ambrosio_vs_chatgpt.png" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/60/ambrosio_vs_chatgpt.png" alt="Ambrosio vs. ChatGPT vs. GPT-4" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;figcaption&gt; &lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;Cool, it's fast and cheap. Is it any good?&lt;/p&gt;
&lt;p&gt;Everyone has their own way of testing the quality of a model. Mine is what I call the  &lt;em&gt;"rice test"&lt;/em&gt;. &lt;a href="https://github.com/duarteocarmo/ambrosio/blob/master/prompts/system.txt"&gt;My system prompt&lt;/a&gt;, stolen from Jeremy Howard contains the following clause:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;if the request begins with the string &amp;quot;vv&amp;quot; [...] make your response as concise as possible
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The question above is a simple and straightforward scenario. I'm at the supermarket, I need to buy some rice to cook with my falafel. I open my phone and: "vv jasmin or basmati rice with falafel?". As you can see above, while GPT-4 gives me &lt;em&gt;exactly&lt;/em&gt; what I need, we can't say the same for Mixtral or GPT-3.5. I just want a quick and dirty answer, I don't want something complicated, I certainly do not want any Python code. So yeah, it doesn't quite pass the jasmin rice test. But we're getting closer. And I will keep testing it in the future.&lt;/p&gt;
&lt;p&gt;And sure, Mixtral may &lt;a href="https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1"&gt;not have been&lt;/a&gt;  trained with a system prompt, but it should, &lt;a href="https://web.archive.org/web/20231030013339/https://docs.mistral.ai/usage/guardrailing/#appendix"&gt;in principle&lt;/a&gt;, support it.&lt;/p&gt;
&lt;h2 id="thoughts-and-taking-ambrosio-further"&gt;Thoughts and taking Ambrosio further&lt;/h2&gt;
&lt;p&gt;Since I ended up using together's API, I decided to also add image generation to Ambrosio (something like I have with DALL·E 2). Together supports most Stable Diffusion models. My feelings about them are largely the same as my feelings about Mixtral and GPT-4. The open source models are good! But they are also a bit rough, and don't come quite close to what Open AI is giving us. At least yet.&lt;/p&gt;
&lt;p&gt;So, will Ambrosio &lt;em&gt;completely&lt;/em&gt; replace my use of GPT-4? Will I cancel my ChatGPT plus subscription? Not really. At least, &lt;em&gt;not yet&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Every time I use these open source models, I feel like I'm missing out on a better answer. In a way, I've become spoiled by GPT-4. It just grasps what I want in a better way, and most importantly, it passes the rice test with flying colors!&lt;/p&gt;
&lt;p&gt;But the truth also is, that for most tasks, Mixtral is good enough. And it's a big leap forward from what the open source community previously had. I can only imagine what comes next.&lt;/p&gt;
&lt;p&gt;Exciting times.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Rebuilding /photos</title><link href="https://duarteocarmo.com/blog/rebuilding-photos.html" rel="alternate"/><published>2024-01-02T22:35:00+01:00</published><updated>2024-01-02T22:35:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2024-01-02:/blog/rebuilding-photos.html</id><summary type="html">&lt;p&gt;A couple of years ago, I decided to remove all my photos from Instagram. I wanted something for myself. Something that suited what I needed. The result was &lt;a href="/photos.html"&gt;/photos&lt;/a&gt;. I wrote about it when I built it &lt;a href="https://duarteocarmo.com/blog/self-hosting-instagram-python.html"&gt;too&lt;/a&gt;. Here's a snippet from that post:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[...] Uploading is by no means as …&lt;/em&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;A couple of years ago, I decided to remove all my photos from Instagram. I wanted something for myself. Something that suited what I needed. The result was &lt;a href="/photos.html"&gt;/photos&lt;/a&gt;. I wrote about it when I built it &lt;a href="https://duarteocarmo.com/blog/self-hosting-instagram-python.html"&gt;too&lt;/a&gt;. Here's a snippet from that post:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;[...] Uploading is by no means as simple as opening an app and snapping a picture. But I've created a small Python script that processes a photo, generates a thumbnail, asks some questions about it, and uploads it to S3 and my blog. And that's good enough [...]&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It wasn't good enough. I guess some &lt;a href="https://course.ccs.neu.edu/cs5500f14/Notes/Prototyping1/planToThrowOneAway.html"&gt;principles&lt;/a&gt; are just timeless. It needed to be thrown away.&lt;/p&gt;
&lt;h2 id="one-to-throw-away"&gt;One to throw away&lt;/h2&gt;
&lt;p&gt;Quickly after making it, it was pretty obvious it was a solution to throw into the trash. In 2 years, I published a little over 10 photos. It's not that I want to publish thousands of photos, but I've always enjoyed taking and sharing them. But that whole run a script thing was just not working for me.&lt;/p&gt;
&lt;p&gt;In a way, it's a lot like writing. Add a little bit of friction to the process, and nobody will use it at all. The script I originally built quickly got lost and stopped working. Whenever I wanted to share a photo, my laptop wasn't around. When the laptop was around, I wasn't thinking of sharing photos. So no, not good enough.&lt;/p&gt;
&lt;p&gt;Whatever I was going to build to replace the old system, needed to do one thing really well: get out of the way.&lt;/p&gt;
&lt;p&gt;I thought about it a bunch. An API? Should I interact with that API via shortcuts? Should I just build an entire front-end? What if I give &lt;a href="https://leptos.dev/"&gt;leptos&lt;/a&gt; a shot? Should I rclone a google photos album? It can't be that hard. I needed something that got out of the way.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/59/ambrosio.png" alt="Ambrosio create photo" style="max-width:95%;border-radius: 2px"&gt;
&lt;figcaption&gt;&lt;/figcaption&gt;
&lt;/center&gt;

&lt;h2 id="ambrosio"&gt;Ambrosio&lt;/h2&gt;
&lt;p&gt;I needed something that was already part of my routine. The result is &lt;a href="https://github.com/duarteocarmo/ambrosio"&gt;Ambrosio&lt;/a&gt;. Some will recognize the &lt;a href="https://youtu.be/oSKi309VnG8?si=92t2m6KNgsX092FX&amp;amp;t=9"&gt;name&lt;/a&gt;. Ambrosio is a Telegram bot that was designed to be my personal assistant.&lt;/p&gt;
&lt;p&gt;I designed Ambrosio to be able to have different &lt;a href="https://github.com/duarteocarmo/ambrosio/tree/master/modes"&gt;&lt;em&gt;modes&lt;/em&gt;&lt;/a&gt;. The first one is the &lt;code&gt;photo&lt;/code&gt; mode. It's responsible for performing &lt;a href="https://www.crowdstrike.com/cybersecurity-101/observability/crud/"&gt;CRUD&lt;/a&gt; operations on an &lt;a href="https://www.cloudflare.com/developer-platform/r2/"&gt;R2 bucket&lt;/a&gt;. To maintain the speed of /photos, it also generates a &lt;code&gt;webp&lt;/code&gt; thumbnail with reduced quality. If needed, it triggers a rebuild of my website using a &lt;a href="https://developers.cloudflare.com/pages/configuration/deploy-hooks/"&gt;deploy hook&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Instead of having to create a markdown page for every new photo, I've built another small pelican &lt;a href="https://github.com/duarteocarmo/duarteocarmo.com/blob/master/plugins/photos/photos.py"&gt;plugin&lt;/a&gt;. Before building the website, it downloads all the metadata for each photo, and generates the pages that it needs to. So that website builds remain fast on my local machine, I also added disk caching. Remember: get out of the way!&lt;/p&gt;
&lt;h2 id="on-go"&gt;On Go&lt;/h2&gt;
&lt;p&gt;I decided to build Ambrosio using something I've never used before: Go. I've tinkered with a couple of code bases but never really used it to build something from scratch. The idea of something not as strict as Rust, but also not as flexible as Python really sparked my curiosity.&lt;/p&gt;
&lt;p&gt;Go gives me mixed feelings, but Go gives me hope. It is strict, but not &lt;em&gt;too&lt;/em&gt; strict. It gives me the confidence when writing code that Python has never given me. And gives me guarantees that if the compiler does not complain, things will mostly work fine. There are some things I still don't understand about Go. The first is error handling. Having a bunch if &lt;code&gt;if err == nil&lt;/code&gt; throughout the code base is not particularly nice to look at. Also hesitant about the whole one letter variable names thing. Things tend to become cryptic.&lt;/p&gt;
&lt;p&gt;It's fun how programming languages all start looking like the same the more you use them. For now, I'm curious to see how my opinion about Go evolves with time.&lt;/p&gt;
&lt;p&gt;One thing is clear though: Whatever technology you use, whatever framework you go with, technology is beautiful when it &lt;em&gt;gets out of the way&lt;/em&gt;.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>You can now listen to this blog</title><link href="https://duarteocarmo.com/blog/you-can-now-listen-to-this-blog.html" rel="alternate"/><published>2023-12-08T05:00:00+01:00</published><updated>2023-12-08T05:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-12-08:/blog/you-can-now-listen-to-this-blog.html</id><summary type="html">&lt;p&gt;One of my favorite &lt;a href="https://www.publico.pt/autor/joao-miguel-tavares"&gt;Portuguese columnists&lt;/a&gt; has this weird thing about his column. Maybe it's more common than I thought. For every piece he publishes, he also publishes a podcast version along with it.&lt;/p&gt;
&lt;p&gt;Now, either &lt;a href="https://www.publico.pt/"&gt;Publico&lt;/a&gt; has 27th century text-to-speech (TTS) technology, or he's &lt;em&gt;actually&lt;/em&gt; reading them. I don't …&lt;/p&gt;</summary><content type="html">&lt;p&gt;One of my favorite &lt;a href="https://www.publico.pt/autor/joao-miguel-tavares"&gt;Portuguese columnists&lt;/a&gt; has this weird thing about his column. Maybe it's more common than I thought. For every piece he publishes, he also publishes a podcast version along with it.&lt;/p&gt;
&lt;p&gt;Now, either &lt;a href="https://www.publico.pt/"&gt;Publico&lt;/a&gt; has 27th century text-to-speech (TTS) technology, or he's &lt;em&gt;actually&lt;/em&gt; reading them. I don't have a problem with that, but I'm &lt;em&gt;pretty&lt;/em&gt; sure we could automate that part of the process with today's tech.&lt;/p&gt;
&lt;p&gt;And yeah, I've heard all the rage about voice cloning services like &lt;a href="https://elevenlabs.io/pricing"&gt;ElevenLabs&lt;/a&gt;. But if you've been following this blog for a while, you probably guessed that we're not just gonna use an API. We're probably gonna build one from scratch.&lt;/p&gt;
&lt;h2 id="an-engine"&gt;An engine&lt;/h2&gt;
&lt;p&gt;The premise did not appear simple to build, but was easy to understand. Something that transcribes every new article of this blog using &lt;em&gt;my own&lt;/em&gt; voice. It needs to be cheap, seamless, and most importantly, not get in the way. Writing is &lt;em&gt;enough&lt;/em&gt; work as is.&lt;/p&gt;
&lt;p&gt;The result is &lt;a href="https://github.com/duarteocarmo/podcaster/"&gt;podcaster&lt;/a&gt;. It runs 100% on GitHub Actions, it scans every new blog post in my RSS feed and uses &lt;a href="https://huggingface.co/coqui/XTTS-v2"&gt;XTTS-v2&lt;/a&gt; to transcribe it. The only thing it needs from me is a 1-min audio file. I tried &lt;a href="https://tts.readthedocs.io/en/latest/models/bark.html"&gt;Bark&lt;/a&gt; and a couple of other models, but this was the only one that made Vittoria come into the room when I was testing things around.&lt;/p&gt;
&lt;p&gt;It's not that I hate infrastructure, I just wanted the whole thing to run on CI. All these TTS models are slow when running on the CPU. But instead of embarking on another painful journey through the world of GPU computing, I found &lt;a href="https://modal.com"&gt;Modal&lt;/a&gt;. I'm happy to report that I've regained faith in the future of serverless GPUs:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# define the image&lt;/span&gt;

&lt;span class="n"&gt;MODAL_IMAGE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;modal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debian_slim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pip_install_from_pyproject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pyproject.toml&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apt_install&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ffmpeg&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;modal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;modal-app&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODAL_IMAGE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# create the function&lt;/span&gt;

&lt;span class="nd"&gt;@stub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;any&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ParsedArticle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;voice_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VOICE_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MODEL_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LANGUAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# this part is executed in a GPU powered machine&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The engine stores all transcripts in S3, automatically generates a podcast feed from them (using &lt;a href="https://feedgen.kiesow.be/"&gt;feedgen&lt;/a&gt;), and uses web hooks to trigger a new build of this blog. Again, pretty much for free. If you're curious or interested in taking it for a spin, &lt;a href="https://github.com/duarteocarmo/podcaster#readme"&gt;should be straightforward&lt;/a&gt; to get started.&lt;/p&gt;
&lt;h2 id="integrating-with-pelican"&gt;Integrating with Pelican&lt;/h2&gt;
&lt;p&gt;As I said before: I really &lt;em&gt;don't&lt;/em&gt; like adding friction to my writing. It's already hard as is! The challenge then was to figure a way of automagically updating the blog with available transcripts after I publish, without me having to do anything at all.&lt;/p&gt;
&lt;p&gt;Fortunately, &lt;a href="https://justinmayer.com/about/"&gt;Justin&lt;/a&gt; besides being great company at PyCon Italia every year, has also built a pretty &lt;a href="https://docs.getpelican.com/en/latest/plugins.html"&gt;robust plugin system for Pelican&lt;/a&gt;. All I had to do, was to add a &lt;a href="https://github.com/duarteocarmo/duarteocarmo.com/blob/master/plugins/podcast/podcast.py"&gt;&lt;code&gt;podcaster&lt;/code&gt; plugin&lt;/a&gt; to this website. The plugin automatically matches articles to corresponding episodes, and adds that short html snippet you're seeing above. Should be &lt;em&gt;build once&lt;/em&gt; and let run. Hopefully at least.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;
&lt;p&gt;I had a lot of fun building this, and also learned a lot. First, it demystified the whole podcast hosting thing for me. Turns out, it's just a bunch of mp3 files in a bucket with &lt;a href="https://podcasts.apple.com/dk/podcast/duarte-o-carmos-articles/id1719493997"&gt;a rss feed&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Often I've settled for running ML models on CPU just because deploying with a GPU was much more of a pain, and didn't add any happiness to the process. At least for now, GPUs are here to stay, hopefully so are services like &lt;a href="https://modal.com/"&gt;Modal&lt;/a&gt;. Pythonic, easy to use, and easy to isolate from the rest of the code base.&lt;/p&gt;
&lt;p&gt;Finally, I was very impressed with the state of TTS and voice cloning technology. Is it perfect? No. Does it have some artifacts? Yes. Does it sound like a robot sometimes? Sure. But remember, I &lt;em&gt;only gave it a minute&lt;/em&gt; of my voice.&lt;/p&gt;
&lt;p&gt;Open source never ceases to amaze me.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Should you go into management consulting? Maybe.</title><link href="https://duarteocarmo.com/blog/management-consulting.html" rel="alternate"/><published>2023-11-08T08:00:00+01:00</published><updated>2023-11-08T08:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-11-08:/blog/management-consulting.html</id><summary type="html">&lt;p&gt;A couple of days ago, someone shared &lt;a href="https://www.youtube.com/watch?v=AiOUojVd6xQ"&gt;this&lt;/a&gt; John Oliver video with me. In it, he shows all the ways McKinsey is a terrible organization. Not a minute in and I was already thinking about that management consulting blog post I &lt;em&gt;always&lt;/em&gt; wanted to write.&lt;/p&gt;
&lt;p&gt;When I talk with people …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A couple of days ago, someone shared &lt;a href="https://www.youtube.com/watch?v=AiOUojVd6xQ"&gt;this&lt;/a&gt; John Oliver video with me. In it, he shows all the ways McKinsey is a terrible organization. Not a minute in and I was already thinking about that management consulting blog post I &lt;em&gt;always&lt;/em&gt; wanted to write.&lt;/p&gt;
&lt;p&gt;When I talk with people that just finished University a lot of them wonder if it's the right place to go. &lt;em&gt;Of course&lt;/em&gt;, by the tasty recruiting videos, it looks like the absolute perfect place for your &lt;em&gt;typical&lt;/em&gt; graduate. The reality is a &lt;em&gt;bit&lt;/em&gt; more nuanced.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Attention&lt;/em&gt; to detail. That was the thing that struck me first. From manically aligning elements on a slide, to someone going meticulously through my email signature to ensure everything is as it should be. If you're the type that struggles with detail you'll probably suffer a bit. Truth is, attention to detail is generally something most working environments admire, and strive for (or maybe they should).&lt;/p&gt;
&lt;p&gt;In a way, management consulting feels like an extension of a University. Everything is &lt;em&gt;pre&lt;/em&gt; structured, everything is &lt;em&gt;pre&lt;/em&gt; defined. Even before you get your hands dirty, you'll know exactly the path that you're expected to follow, and how fast you're expected to follow it. &lt;em&gt;Up or out&lt;/em&gt;, that's a good summary of the approach. If you're not being promoted, then you should probably get out. Naturally, most people stay 2 years and leave. In any case - a structured and defined environment is where many are able to learn at their best.&lt;/p&gt;
&lt;p&gt;What I miss the most is by far the &lt;em&gt;people&lt;/em&gt;. I know - I sound just like a propaganda boy. But there's nothing I enjoy more than being the dumbest in the room. Most people in these places are driven, motivated, and hard-working. You'll meet the smartest people you will probably ever work with. Like in the military, you'll make long lasting bonds over traumatic experiences (e.g., long nights, tough clients). You'll build a network of great people - with whom you'll stay connected for the rest of your career. I certainly did.&lt;/p&gt;
&lt;p&gt;But it's not all rainbows and unicorns.&lt;/p&gt;
&lt;p&gt;Let's start with the thing most people fear. The hours. Yes, the hours are long. I generally don't dislike long hours. With an important premise: I &lt;em&gt;have&lt;/em&gt; to be doing something I &lt;em&gt;really&lt;/em&gt; enjoy. But in these firms, the hours are not long because they &lt;em&gt;need&lt;/em&gt; to be. The hours are long because you want to show your client (and your firm) that you are &lt;em&gt;always&lt;/em&gt; available. Sure, this means working longer hours - but less intense hours. For some, this will look like an absolute waste of time - why the hell are we here? It's not about doing the required work. It's about &lt;em&gt;being there&lt;/em&gt;. Or worse, it's about &lt;em&gt;showing&lt;/em&gt; that you were there.&lt;/p&gt;
&lt;p&gt;Soon, you'll realize you don't really have &lt;em&gt;choices&lt;/em&gt;. You &lt;em&gt;don't&lt;/em&gt; get to decide what projects you work on. You &lt;em&gt;don't&lt;/em&gt; get to decide what business area you specialize in. You &lt;em&gt;don't&lt;/em&gt; get to decide when and where you work. You don't get to decide what dinners, events, and gatherings you go to (a common strategy to work a bit less). If you have no clue what you want to do - this is great. You just need to go along. If you know what you want to do - this becomes &lt;em&gt;problematic&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Which brings me to my last point. Management consulting, from my experience, is a mix of a University and Military Service. You are &lt;em&gt;not&lt;/em&gt; rewarded for thinking differently. You are &lt;em&gt;not&lt;/em&gt; rewarded for exploring things others have not. Your innovative way of solving a problem is a risk, rather than an opportunity. For those who just want to do work, this is great - you don't really have to think about it. For the creatives, that want to spend time in things they like, and want to solve problems in crazy ways - this can be painful.&lt;/p&gt;
&lt;p&gt;Is management consulting for you? I have no clue.&lt;/p&gt;
&lt;p&gt;My experience was positive. Until it wasn't. It can be great step for those of us who have absolutely no idea what they want to do after university. It's an opportunity to see a lot of different things - with a smart bunch of people. For those who know exactly what they want - it might feel like a slow down. For most of us, it's a mix.&lt;/p&gt;
&lt;p&gt;In any case. At least now you know what you're getting yourself into.&lt;/p&gt;
&lt;p&gt;I sure wasn't. But I can't say I regret it.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Disclaimer: This post is highly biased towards my experience. As it should - this is MY blog after all.&lt;/em&gt;&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Changelog neural search</title><link href="https://duarteocarmo.com/blog/changelog-neural-search-superduperdb.html" rel="alternate"/><published>2023-10-06T05:00:00+02:00</published><updated>2023-10-06T05:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-10-06:/blog/changelog-neural-search-superduperdb.html</id><summary type="html">&lt;p&gt;Search is one of the most important breakthroughs of the internet. Some are saying a list of blue links is not enough - and that AI &lt;em&gt;will&lt;/em&gt; overthrow search. I don't know if we're about to witness a revolution. But as with most things - there's only one way to know - to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Search is one of the most important breakthroughs of the internet. Some are saying a list of blue links is not enough - and that AI &lt;em&gt;will&lt;/em&gt; overthrow search. I don't know if we're about to witness a revolution. But as with most things - there's only one way to know - to build and use it &lt;em&gt;myself&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;I like podcasts a lot. There's really nothing like hearing people talk about things I know &lt;em&gt;nothing&lt;/em&gt; about. Some of my favorite podcasts are produced by the &lt;a href="https://changelog.com/"&gt;Changelog&lt;/a&gt; network. More than once, I've had to use their &lt;a href="https://changelog.com/search?q=embedding"&gt;search engine&lt;/a&gt; when researching something that was said during an episode.&lt;/p&gt;
&lt;p&gt;One of the best things about the Changelog is that the &lt;a href="https://github.com/thechangelog"&gt;whole thing is open source&lt;/a&gt;. From the podcast engine itself, to the &lt;a href="https://github.com/thechangelog/transcripts"&gt;transcripts of every episode&lt;/a&gt;. Why not take all of these transcripts and build an AI-powered™ search engine around them?&lt;/p&gt;
&lt;center&gt;
&lt;a href="https://changelog.duarteocarmo.com"&gt;
&lt;img src="https://duarteocarmo.com/images/56/search.png" alt="Neural search for the changelog" style="max-width:100%;margin-bottom:-1em;"&gt;
&lt;/a&gt;
&lt;figcaption&gt;&lt;a target="_blank" href="https://changelog.duarteocarmo.com"&gt;changelog.duarteocarmo.com&lt;/a&gt;&lt;/figcaption&gt;
&lt;/center&gt;

&lt;h2 id="how-its-built"&gt;How it's built&lt;/h2&gt;
&lt;p&gt;Before I describe the stack, let's get the obvious out of the way: the whole thing is open source. Both the &lt;a href="https://github.com/duarteocarmo/thechangelogbot-backend"&gt;back-end&lt;/a&gt; and the &lt;a href="https://github.com/duarteocarmo/thechangelogbot-frontend"&gt;front-end&lt;/a&gt;. If you prefer to go and poke around the code yourself, be my guest.&lt;/p&gt;
&lt;p&gt;I love the chunk-embed-search-retrieve dance as much as the next guy, but for this one, I wanted to keep things a bit simpler, so I'm letting &lt;a href="https://www.superduperdb.com/"&gt;SuperDuperDB&lt;/a&gt; do most of the heavy lifting for me. With it, all I really need to do is add the embedding model to my serverless MongoDB instance, and it handles the rest for me:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# add model to DB&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SentenceTransformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;VectorIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;identifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;index_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;indexing_listener&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Listener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# search the DB&lt;/span&gt;

&lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;$regex&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;podcast&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;practicalai&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;What are embeddings&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vector_index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;index_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For the front-end, I finally took &lt;a href="https://nextjs.org/"&gt;NextJS&lt;/a&gt; for a spin. Love the productivity gains - especially when we're talking about developer experience. &lt;a href="https://vercel.com/"&gt;Vercel&lt;/a&gt; is absolutely killing the developer experience side of things. On the other side, I have no clue how most of the magic is working - and I'm not sure that's a good thing.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>A poor man's guide to fine-tuning Llama 2</title><link href="https://duarteocarmo.com/blog/fine-tune-llama-2-telegram.html" rel="alternate"/><published>2023-09-26T13:00:00+02:00</published><updated>2023-09-26T13:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-09-26:/blog/fine-tune-llama-2-telegram.html</id><summary type="html">&lt;p&gt;Last Friday, &lt;a href="https://orbit.dtu.dk/en/persons/konstantinos-kalogeropoulos"&gt;Kostas&lt;/a&gt; and I found ourselves discussing AI over beers again. He was telling me how he thinks everyone else is &lt;em&gt;decades&lt;/em&gt; away from OpenAI - and that they likely won't catch up soon. I disagreed. In &lt;em&gt;fact&lt;/em&gt;, I think open source is quickly catching up, and getting &lt;a href="https://huggingface.co/spaces/lmsys/chatbot-arena-leaderboard"&gt;closer&lt;/a&gt; by …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Last Friday, &lt;a href="https://orbit.dtu.dk/en/persons/konstantinos-kalogeropoulos"&gt;Kostas&lt;/a&gt; and I found ourselves discussing AI over beers again. He was telling me how he thinks everyone else is &lt;em&gt;decades&lt;/em&gt; away from OpenAI - and that they likely won't catch up soon. I disagreed. In &lt;em&gt;fact&lt;/em&gt;, I think open source is quickly catching up, and getting &lt;a href="https://huggingface.co/spaces/lmsys/chatbot-arena-leaderboard"&gt;closer&lt;/a&gt; by the day.&lt;/p&gt;
&lt;p&gt;Around four months ago, I &lt;a href="/blog/fine-tune-flan-t5-telegram.html"&gt;wrote a small tutorial&lt;/a&gt; on fine-tuning Flan T5 to generate conversations in my friends group chat. It worked &lt;em&gt;ok(ish)&lt;/em&gt;, but the process was clunky and took way too long.&lt;/p&gt;
&lt;p&gt;So, here we are, four months later, with the same question: How easy is it to train an LLM on my friend's group chat? Turns out, for a couple of dollars and one hour you can get pretty far.&lt;/p&gt;
&lt;center&gt;
&lt;br&gt;
&lt;video  style="max-width:100%;border-radius: 2px" autoplay loop muted playsinline&gt;
  &lt;source src="https://duarteocarmo.com/images/55/llama-tiger-optimized.mp4" type="video/mp4" &gt;
&lt;/video&gt;
&lt;figcaption&gt;&lt;em&gt;Tiger-llama in action, simulating a Telegram conversation&lt;/em&gt;&lt;/figcaption&gt;
&lt;/center&gt;

&lt;h2 id="data"&gt;Data&lt;/h2&gt;
&lt;p&gt;The goal is clear: fine-tune &lt;a href="https://huggingface.co/meta-llama/Llama-2-7b-hf"&gt;Llama 2&lt;/a&gt;, to automatically generate conversations that would normally happen in the group chat that I've held with my close friends for years now. The first step is to export the data from Telegram (which is pretty &lt;a href="https://telegram.org/blog/export-and-more"&gt;easy&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;For most LLMs, you need to massage the data into a specific format before training. This means providing things like the &lt;code&gt;instruction&lt;/code&gt;, the &lt;code&gt;input&lt;/code&gt;, the &lt;code&gt;system_prompt&lt;/code&gt;, etc. But all I wanted was to generate conversations, so I started by creating a large &lt;code&gt;jsonl&lt;/code&gt; file with chunks of conversations separated by a &lt;code&gt;###&lt;/code&gt; token to indicate the speaker.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# dataset.jsonl&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;### Friend 1: This is a question### Friend 2: This is a reply### Friend 3: What the hell are you guys talking about?&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;### Friend 4: Who&amp;#39;s coming tonight?### Friend 2: No one, it&amp;#39;s literally Monday.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;#...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After getting the data ready, I went ahead and pushed it to the hugging face hub:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# huggingface-cli login --token hf_XXXXXXXX (to log into Hugging Face)&lt;/span&gt;

&lt;span class="c1"&gt;# read and split&lt;/span&gt;

&lt;span class="n"&gt;tiger_llama&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dataset.jsonl&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;train_tiger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_tiger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tiger_llama&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# push to hub&lt;/span&gt;

&lt;span class="n"&gt;train_tiger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pandas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train_tiger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;test_tiger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pandas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_tiger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DatasetDict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;train&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;train_tiger&lt;/span&gt;
&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;test&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;test_tiger&lt;/span&gt;

&lt;span class="n"&gt;dataset_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;duarteocarmo/tiger-llama&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push_to_hub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;main&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;private&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With all the conversations pushed to the hub (&lt;em&gt;cough&lt;/em&gt; - privacy - &lt;em&gt;cough&lt;/em&gt;), it was now time to train this model.&lt;/p&gt;
&lt;h2 id="training-with-axolotl"&gt;Training with axolotl&lt;/h2&gt;
&lt;p&gt;For the first couple of days, I got a bit frustrated. I tested a bunch of &lt;a href="https://huggingface.co/blog/llama2#fine-tuning-with-peft"&gt;different&lt;/a&gt; &lt;a href="https://github.com/brevdev/notebooks/blob/main/llama2-finetune.ipynb"&gt;tutorials&lt;/a&gt; on how to fine tune LLama-2. But none of them really got me anywhere. Some took way too long to train, others resulted in a model that didn't really generate anything interesting. I confess I &lt;a href="https://platform.openai.com/docs/guides/fine-tuning"&gt;almost fell to the dark side&lt;/a&gt;! But then I stumbled upon &lt;a href="https://github.com/OpenAccess-AI-Collective/axolotl"&gt;axolotl&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/OpenAccess-AI-Collective/axolotl"&gt;Axolotl&lt;/a&gt; is designed to "&lt;em&gt;streamline the fine-tuning of LLMs&lt;/em&gt;". It supports a bunch of different models and training configurations. The best part? To fine-tune a model, all you is pretty much a config file. Yes, you heard that right - &lt;em&gt;just&lt;/em&gt; a &lt;code&gt;yaml&lt;/code&gt; file. The only things I needed to tweak were the base model (in our case, Llama 2 - &lt;code&gt;meta-llama/Llama-2-7b-hf&lt;/code&gt;), and the dataset sections:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# llama-tiger.yaml&lt;/span&gt;

&lt;span class="c1"&gt;# base model&lt;/span&gt;

&lt;span class="nt"&gt;base_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;meta-llama/Llama-2-7b-hf&lt;/span&gt;
&lt;span class="nt"&gt;base_model_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;meta-llama/Llama-2-7b-hf&lt;/span&gt;
&lt;span class="nt"&gt;model_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;LlamaForCausalLM&lt;/span&gt;
&lt;span class="nt"&gt;tokenizer_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;LlamaTokenizer&lt;/span&gt;
&lt;span class="nt"&gt;is_llama_derived_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="c1"&gt;# my dataset&lt;/span&gt;

&lt;span class="c1"&gt;# other formats: https://github.com/OpenAccess-AI-Collective/axolotl#dataset&lt;/span&gt;

&lt;span class="nt"&gt;datasets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;duarteocarmo/tiger-llama&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;completion&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;text&lt;/span&gt;
&lt;span class="nt"&gt;dataset_prepared_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;last_run_prepared&lt;/span&gt;
&lt;span class="nt"&gt;hub_model_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;duarteocarmo/tiger-llama&lt;/span&gt;
&lt;span class="nt"&gt;val_set_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0.01&lt;/span&gt;
&lt;span class="nt"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;./qlora-out&lt;/span&gt;

&lt;span class="c1"&gt;# wandb monitoring&lt;/span&gt;

&lt;span class="nt"&gt;wandb_project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;llama-tiger&amp;quot;&lt;/span&gt;
&lt;span class="nt"&gt;wandb_log_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;checkpoint&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;### ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;details&gt;
  &lt;summary&gt;Expand for the full &lt;code&gt;tiger-llama.yaml&lt;/code&gt; file&lt;/summary&gt;


&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Image: winglian/axolotl:main-py3.10-cu118-2.0.1&lt;/span&gt;

&lt;span class="nt"&gt;base_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;meta-llama/Llama-2-7b-hf&lt;/span&gt;
&lt;span class="nt"&gt;base_model_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;meta-llama/Llama-2-7b-hf&lt;/span&gt;
&lt;span class="nt"&gt;model_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;LlamaForCausalLM&lt;/span&gt;
&lt;span class="nt"&gt;tokenizer_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;LlamaTokenizer&lt;/span&gt;
&lt;span class="nt"&gt;is_llama_derived_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;

&lt;span class="nt"&gt;load_in_8bit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;
&lt;span class="nt"&gt;load_in_4bit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="nt"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;

&lt;span class="nt"&gt;datasets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;duarteocarmo/tiger-llama&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;completion&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;text&lt;/span&gt;
&lt;span class="nt"&gt;dataset_prepared_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;last_run_prepared&lt;/span&gt;
&lt;span class="nt"&gt;hub_model_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;duarteocarmo/tiger-llama&lt;/span&gt;
&lt;span class="nt"&gt;val_set_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0.01&lt;/span&gt;
&lt;span class="nt"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;./qlora-out&lt;/span&gt;

&lt;span class="nt"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;qlora&lt;/span&gt;
&lt;span class="nt"&gt;lora_model_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="nt"&gt;sequence_len&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;4096&lt;/span&gt;
&lt;span class="nt"&gt;sample_packing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="nt"&gt;pad_to_sequence_len&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;

&lt;span class="nt"&gt;lora_r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;32&lt;/span&gt;
&lt;span class="nt"&gt;lora_alpha&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;16&lt;/span&gt;
&lt;span class="nt"&gt;lora_dropout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0.05&lt;/span&gt;
&lt;span class="nt"&gt;lora_target_modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;lora_target_linear&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="nt"&gt;lora_fan_in_fan_out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="nt"&gt;wandb_project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;llama-tiger&amp;quot;&lt;/span&gt;
&lt;span class="nt"&gt;wandb_entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;wandb_watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;wandb_run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;wandb_log_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;checkpoint&amp;quot;&lt;/span&gt;

&lt;span class="nt"&gt;gradient_accumulation_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;4&lt;/span&gt;
&lt;span class="nt"&gt;micro_batch_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;2&lt;/span&gt;
&lt;span class="nt"&gt;num_epochs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;3&lt;/span&gt;
&lt;span class="nt"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;paged_adamw_32bit&lt;/span&gt;
&lt;span class="nt"&gt;lr_scheduler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;cosine&lt;/span&gt;
&lt;span class="nt"&gt;learning_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0.0002&lt;/span&gt;

&lt;span class="nt"&gt;train_on_inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;
&lt;span class="nt"&gt;group_by_length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;
&lt;span class="nt"&gt;bf16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="nt"&gt;fp16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;
&lt;span class="nt"&gt;tf32&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;false&lt;/span&gt;

&lt;span class="nt"&gt;gradient_checkpointing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="nt"&gt;early_stopping_patience&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;resume_from_checkpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;local_rank&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;logging_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;1&lt;/span&gt;
&lt;span class="nt"&gt;xformers_attention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;flash_attention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;

&lt;span class="nt"&gt;warmup_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;10&lt;/span&gt;
&lt;span class="nt"&gt;eval_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;20&lt;/span&gt;
&lt;span class="nt"&gt;eval_table_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;5&lt;/span&gt;
&lt;span class="nt"&gt;save_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;deepspeed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;weight_decay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;0.0&lt;/span&gt;
&lt;span class="nt"&gt;fsdp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;fsdp_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;special_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;bos_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;lt;s&amp;gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;eos_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;lt;/s&amp;gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;unk_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;lt;unk&amp;gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;/details&gt;

&lt;p&gt;Unfortunately, with these things, I needed a GPU to run the training. Given that I'm (&lt;em&gt;apparently&lt;/em&gt;) considered &lt;a href="https://www.businessinsider.com/gpu-rich-vs-gpu-poor-tech-companies-in-each-group-2023-8?r=US&amp;amp;IR=T"&gt;GPU poor&lt;/a&gt;, I used &lt;a href="https://vast.ai/"&gt;Vast.ai&lt;/a&gt;. I got a machine with at least ~40GB of GPU RAM, and that was relatively close to me. These cost around 1 USD/hour. I also used the Axololt docker image (e.g., &lt;code&gt;winglian/axolotl:main-py3.10-cu118-2.0.1&lt;/code&gt;) so that everything was pre-installed when the machine turned on.&lt;/p&gt;
&lt;p&gt;Once the machine was up, I ssh'd into it and ran:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;huggingface-cli&lt;span class="w"&gt; &lt;/span&gt;login&lt;span class="w"&gt; &lt;/span&gt;--token&lt;span class="w"&gt; &lt;/span&gt;hf_MY_HUGGINGFACE_TOKEN_WITH_WRITE_ACCESS
$&lt;span class="w"&gt; &lt;/span&gt;wandb&lt;span class="w"&gt; &lt;/span&gt;login&lt;span class="w"&gt; &lt;/span&gt;MY_WANDB_API_KEY
$&lt;span class="w"&gt; &lt;/span&gt;accelerate&lt;span class="w"&gt; &lt;/span&gt;launch&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;axolotl.cli.train&lt;span class="w"&gt; &lt;/span&gt;llama-tiger.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And we were off to the races. With a single command, I was fine-tuning Llama 2 on my custom dataset. While training, Axolotl automatically logs everything to Weights &amp;amp; Biases, so we can monitor how the losses are evolving. As a bonus, it also shows the model outputs so that I can follow how to model is improving its generation during training:&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/55/monitoring.png" alt="wandb monitoring dashboard" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;My dataset has ~12K rows. Training took around 1 hour in total, using a machine with 2xA40 GPUs. In summary: &lt;strong&gt;I spent around 2 USD to fine-tune the whole model&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Once training was done, your fine-tuned model (or &lt;a href="https://huggingface.co/docs/transformers/main/peft"&gt;adapter&lt;/a&gt;, to be specific) is saved in the &lt;code&gt;./lora-out&lt;/code&gt; directory. With my configuration, it was also uploaded the model to the hugging face repository I provided in &lt;code&gt;hub_model_id&lt;/code&gt;. Onto the inference.&lt;/p&gt;
&lt;h2 id="inference"&gt;Inference&lt;/h2&gt;
&lt;p&gt;The fine-tuning result is not an actual Llama 2 model, but an &lt;em&gt;adapter&lt;/em&gt; to the model (Axolotl uses &lt;a href="https://github.com/artidoro/qlora"&gt;qlora&lt;/a&gt; by default for Llama models). So in the end, the adapter is a mere 320 MB.&lt;/p&gt;
&lt;p&gt;Using Axolotl, inference is also pretty straightforward: All I need to do is download the model, and launch the Axolotl inference command:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# download from fine tuned repo&lt;/span&gt;

git&lt;span class="w"&gt; &lt;/span&gt;lfs&lt;span class="w"&gt; &lt;/span&gt;install
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://huggingface.co/duarteocarmo/tiger-llama

&lt;span class="c1"&gt;# run axolotl inference&lt;/span&gt;

accelerate&lt;span class="w"&gt; &lt;/span&gt;launch&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;axolotl.cli.inference&lt;span class="w"&gt; &lt;/span&gt;tiger-llama.yaml&lt;span class="w"&gt; &lt;/span&gt;--lora_model_dir&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;./tiger-llama&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once downloaded and launched, I can give the model the start of a fake conversation, and it will go on to generate a completely fake conversation based on my group chat:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Input:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;### SZ: Quem vem a Lisboa no natal?&lt;/span&gt;
Output:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;### SZ: Quem vem a Lisboa no natal?### PK: Acho que tenho tive uma surpresa de aniversario da malta, não tenho férias até janeiro### SZ: Fodass, faltamos 2 ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Compared to the &lt;a href="/blog/fine-tune-flan-t5-telegram.html"&gt;last&lt;/a&gt; time I fine-tuned a model, open source is definitely moving fast. The process was not only much faster, and simpler than fine-tuning Flan T5 using a notebook, but the results were also much better than anything I had seen so far.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/OpenAccess-AI-Collective/axolotl"&gt;Axolotl&lt;/a&gt; did almost all the heavy lifting. Making the whole process super smooth. All I needed was a dataset, a config file, 2 USD, and about an hour to fine-tune a model. We've come a &lt;em&gt;long&lt;/em&gt; way.&lt;/p&gt;
&lt;p&gt;The model is still not perfect though. It captures some of my friends' quirks and ways of speaking, and the generated conversations make sense around ~70% of the time. But that's &lt;em&gt;still&lt;/em&gt; 30% nonsense. Could be a couple of things: the size (I used the "smaller" 7 billion version of the model), or even the language (Portuguese from Portugal). For example, when I prompt the model to simulate a politics discussion between my friends, someone starts discussing something about &lt;a href="https://da.wikipedia.org/wiki/Dilma_Rousseff"&gt;Dilma&lt;/a&gt; (which is &lt;em&gt;wildly&lt;/em&gt; inaccurate given we're from Portugal).&lt;/p&gt;
&lt;p&gt;So Kostas was right, &lt;a href="https://opensourceconnections.com/blog/2023/07/19/is-llama-2-open-source-no-and-perhaps-we-need-a-new-definition-of-open/"&gt;"open source"&lt;/a&gt; hasn't caught up yet. But oh boy, we're getting damn close.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>aicoverlettercreator.com</title><link href="https://duarteocarmo.com/blog/ai-cover-letter-creator-django-ai.html" rel="alternate"/><published>2023-07-19T13:00:00+02:00</published><updated>2023-07-19T13:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-07-19:/blog/ai-cover-letter-creator-django-ai.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://aicoverlettercreator.com?ref=blog_post" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/54/cover.png" alt="aicoverlettercreator.com" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;&lt;em&gt;Get rid of all applications that don't have a cover letter&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I remember it like it was yesterday. As I was leaving one of the first companies I've ever worked for, my manager asked me to hire my replacement. Drowning in hundreds of applications for the position, I still recall …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://aicoverlettercreator.com?ref=blog_post" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/54/cover.png" alt="aicoverlettercreator.com" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;&lt;em&gt;Get rid of all applications that don't have a cover letter&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I remember it like it was yesterday. As I was leaving one of the first companies I've ever worked for, my manager asked me to hire my replacement. Drowning in hundreds of applications for the position, I still recall his exact words.&lt;/p&gt;
&lt;p&gt;I don't want to start a debate on if you should include a cover letter in your job application or not. I've heard good arguments on both sides. There's one thing I'd like to point out though: in the days of ChatGPT, it's hard to get a good excuse &lt;em&gt;not&lt;/em&gt; to write one.&lt;/p&gt;
&lt;p&gt;Enter: &lt;a href="https://aicoverlettercreator.com?ref=blog_post" target="_blank"&gt;aicoverlettercreator.com&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="why-i-built-it"&gt;Why I built it&lt;/h2&gt;
&lt;p&gt;The short answer: &lt;em&gt;Why not?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The long answer, for a couple of reasons.&lt;/p&gt;
&lt;p&gt;I like mentoring/helping people that are just entering the industry (or just getting out of University) regarding their careers. Most people, when applying for positions, don't really pay close attention to the cover letter. I believe it can be a really big differentiator. Even though I hope they are aware of just how much ChatGPT can help them, I integrated in this version a little bit of &lt;em&gt;secret juice&lt;/em&gt;. I think that secret juice will make those cover letters even better. And that's the first reason, to help people.&lt;/p&gt;
&lt;p&gt;The second one is a bit more selfish. I've built a handful of LLM powered applications, but never anything completely end-to-end. They say the best way to learn about something is to build it yourself right? I certainly &lt;a href="/blog/nftuga-nft-experimentation.html"&gt;think so&lt;/a&gt;. So I wanted to build something where I made all the choices. Or something where I felt all the pain.&lt;/p&gt;
&lt;h2 id="the-stack"&gt;The stack&lt;/h2&gt;
&lt;p&gt;I'm not the biggest fan of falling in-love with a stack. "What stack would you use?" Well, tell me about the problem first.&lt;/p&gt;
&lt;p&gt;For this app, I went with &lt;a href="https://www.djangoproject.com/"&gt;Django&lt;/a&gt; and &lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt; (&lt;a href="/blog/infrequent.html"&gt;again&lt;/a&gt;). Django is probably overkill for most &lt;em&gt;simple&lt;/em&gt; web applications, but when users, databases, and settings start becoming a thing, it includes almost all the batteries I need. As for htmx, it's not that I don't like React, it's that I love keeping things simple. That &lt;a href="https://htmx.org/essays/when-to-use-hypermedia/#hypermedia-not-a-good-fit-if"&gt;&lt;em&gt;does not&lt;/em&gt;&lt;/a&gt; mean it's always a great fit. But for this small project, it sure was.&lt;/p&gt;
&lt;p&gt;One of the biggest hurdles when building this, was the implementation of streaming from OpenAI's API. There's nothing I hate more than clicking a button and having to wait 10 seconds for something to appear on screen. Turns out, integrating &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events"&gt;server-sent events&lt;/a&gt; with Django is not so straightforward. And even though Django supports &lt;a href="https://docs.djangoproject.com/en/4.2/ref/request-response/#streaminghttpresponse-objects"&gt;streaming responses&lt;/a&gt;. At the end of the day, you can't really escape some good old Javascript. And I'm ok with that.&lt;/p&gt;
&lt;p&gt;Still sticking to my premise of keeping things as simple as possible. Every component I add to an app during development is a component I'll have to maintain during its lifetime. And I'm not the biggest fan of maintenance work. Because of this, &lt;a href="https://aicoverlettercreator.com"&gt;aicoverlettercreator.com&lt;/a&gt; is both simple to use &lt;em&gt;and&lt;/em&gt; to operate. It's a simple Django app with no &lt;a href="https://docs.celeryq.dev/en/stable/django/first-steps-with-django.html"&gt;queues&lt;/a&gt; and no self hosted PostgreSQL containers. It's a Dockerfile that connects to a &lt;a href="https://planetscale.com/"&gt;PlanetScale&lt;/a&gt; database. That's it, every git commit automatically updates the app.&lt;/p&gt;
&lt;p&gt;Nothing like keeping things simple.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Supercharging my Telegram group with the help of ChatGPT</title><link href="https://duarteocarmo.com/blog/supercharging-telegram-bot-chatgpt-python.html" rel="alternate"/><published>2023-06-10T02:00:00+02:00</published><updated>2023-06-10T02:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-06-10:/blog/supercharging-telegram-bot-chatgpt-python.html</id><summary type="html">&lt;p&gt;While most people in Europe use WhatsApp, my group of friends and I use Telegram. For years now we've used things like &lt;a href="https://combot.org/"&gt;Combot&lt;/a&gt; to see who's more silent than usual, and &lt;a href="https://missrose.org/"&gt;MissRose&lt;/a&gt; to give our monthly elected group admins moderation rights. Yeah, we take friendship that seriously.&lt;/p&gt;
&lt;p&gt;But now we …&lt;/p&gt;</summary><content type="html">&lt;p&gt;While most people in Europe use WhatsApp, my group of friends and I use Telegram. For years now we've used things like &lt;a href="https://combot.org/"&gt;Combot&lt;/a&gt; to see who's more silent than usual, and &lt;a href="https://missrose.org/"&gt;MissRose&lt;/a&gt; to give our monthly elected group admins moderation rights. Yeah, we take friendship that seriously.&lt;/p&gt;
&lt;p&gt;But now we have access to LLMs right? So I decided to build two new features for our group chat. One is &lt;em&gt;pretty&lt;/em&gt; useful, and the other.. Well, you be the judge.&lt;/p&gt;
&lt;h2 id="summarize-the-conversation-with-resume"&gt;Summarize the conversation with &lt;code&gt;/resume&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Our group chat can get pretty active sometimes. We call those the &lt;em&gt;golden hours&lt;/em&gt;. If you're distracted and miss one of those, you'll quickly lose track of what's going on. But let's be honest, sometimes, we just don't have time to catch up.&lt;/p&gt;
&lt;p&gt;Enter the &lt;code&gt;/resume&lt;/code&gt; command. Now, when someone misses a specially hectic part of the conversation, they can just use the /resume command to get a short summary of what happened.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/53/resume_command.png" alt="ChatGPT resume command in Telegram" style="max-width:50%;border-radius: 2px"&gt;
&lt;figcaption&gt;In Portuguese "resume" means "summarize"&lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;To build it I used &lt;a href="https://python.langchain.com/en/latest/index.html"&gt;LangChain&lt;/a&gt; and the cheaper &lt;code&gt;gpt-3.5-turbo&lt;/code&gt; API (e.g., ChatGPT). I keep a rotating list of the last 50 messages that happened in our group. When the command is called, I send those to OpenAI to get a summary back.&lt;/p&gt;
&lt;p&gt;Here's the core part of that code:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/bot/summarizer/main.py&lt;/span&gt;

&lt;span class="n"&gt;LLM_CHAT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatOpenAI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;list_of_messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Fetches the summary for the last 50 messages&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="n"&gt;formatted_list_of_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;list_of_messages&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;formatted_list_of_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;truncate_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;formatted_list_of_message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;system_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;You are an assistant helping friends catch up in a busy chat group. Your goal is to help friends in this group stay up to date without having to read all the messages.&lt;/span&gt;

&lt;span class="s2"&gt;You will receive a recent conversation that happened in the group. Respond immediately with a short and concise summary of the conversation.&lt;/span&gt;
&lt;span class="s2"&gt;The summary should have the following characteristics:&lt;/span&gt;
&lt;span class="s2"&gt;- Should be in Portuguese&lt;/span&gt;
&lt;span class="s2"&gt;- Should have a tone that is similar to the conversation, act like you are part of the group&lt;/span&gt;
&lt;span class="s2"&gt;- Use 3 sentences or less&lt;/span&gt;
&lt;span class="s2"&gt;- Don&amp;#39;t be too general, mention who said what&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="n"&gt;human_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;CONVERSATION BLOCK START&lt;/span&gt;
&lt;span class="si"&gt;{list_of_messages}&lt;/span&gt;
&lt;span class="s2"&gt;CONVERSATION BLOCK END&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="n"&gt;system_message_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SystemMessagePromptTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;system_template&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;human_message_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HumanMessagePromptTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;human_template&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;chat_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;system_message_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;human_message_prompt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;chat_chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LLMChain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;LLM_CHAT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chat_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat_chain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;list_of_messages&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;formatted_list_of_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Response:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Most of my friends really liked the &lt;code&gt;/resume&lt;/code&gt; command. Some however, showed concerns regarding AI and how these things are super scary. After those comments, one thing was obvious: I needed to build something even funnier.&lt;/p&gt;
&lt;h2 id="impersonate-a-user-with-fake-username"&gt;Impersonate a user with &lt;code&gt;/fake @username&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;What if the bot could impersonate any of my friends in the group chat? &lt;em&gt;What if&lt;/em&gt; I could ask the bot to answer a question just like the person X would?&lt;/p&gt;
&lt;p&gt;Enter the &lt;code&gt;/fake @username &amp;lt;insert question&amp;gt;&lt;/code&gt; command. With it, you can impersonate anyone on the group chat (cough, my friends), and ask it to answer just like that person would! Here's the command in action:&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/53/impersonate_command.png" alt="ChatGPT impersonate command in Telegram" style="max-width:50%;border-radius: 2px"&gt;
&lt;figcaption&gt;The `/fake` command in action &lt;/figcaption&gt;
&lt;/center&gt;

&lt;p&gt;Although not as useful as the summarization command, it's actually a bit more complex to build.&lt;/p&gt;
&lt;p&gt;The first component is a vector database. Here, I'm storing the embeddings for pretty much everything my friends said in the past year. I wanted something simple like sqlite so I went with &lt;a href="https://www.trychroma.com/"&gt;Chroma&lt;/a&gt;. The trick here is not to embed every single message separately, but to build a long string of every single thing a person said. Once that's built,  you then chunk it and store it appropriately. With Chroma I could also store metadata about the document - which I used to store the author of the chunks.&lt;/p&gt;
&lt;p&gt;Once the whole vector database is built, I can now retrieve the N pieces of text that are most similar to a particular question (while filtering those results for a particular user):&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/bot/replier/main.py&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;query_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Gets the most relevant n_results items from a person for a given query&lt;/span&gt;
&lt;span class="sd"&gt;    and returns them as a context&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;query_texts&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;n_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;from&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;documents&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;No documents found&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now that I could get the most related items to a certain query for a particular user, I could start putting the impersonation together. Here, the answering chain function is in charge of trying to impersonate a user to the best of its ability given a question and relevant context:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/bot/replier/main.py&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;build_answering_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;LLMChain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Builds a answering chain for the impersonation&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="n"&gt;system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;Your name is &lt;/span&gt;&lt;span class="si"&gt;{person}&lt;/span&gt;&lt;span class="s2"&gt;. You are participating in a group chat with all of your childhood friends.&lt;/span&gt;
&lt;span class="s2"&gt;- You will be given some context of messages from &lt;/span&gt;&lt;span class="si"&gt;{person}&lt;/span&gt;
&lt;span class="s2"&gt;- Use the context to inspire your answer to the user question&lt;/span&gt;
&lt;span class="s2"&gt;- Use the same tone of voice and writing style as the messages in the context&lt;/span&gt;
&lt;span class="s2"&gt;- If the answer is not in the context, make something funny up&lt;/span&gt;
&lt;span class="s2"&gt;- Always answer from the perspective of being &lt;/span&gt;&lt;span class="si"&gt;{person}&lt;/span&gt;
&lt;span class="s2"&gt;- Remember to be funny and entertaining&lt;/span&gt;
&lt;span class="s2"&gt;- This is a group chat and everything is fun and entertaining, so insults are allowed and fun&lt;/span&gt;
&lt;span class="s2"&gt;----------------&lt;/span&gt;
&lt;span class="si"&gt;{context}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;system_message_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SystemMessagePromptTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;system_prompt&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;human_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{question}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;human_message_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HumanMessagePromptTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;human_prompt&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;chat_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;system_message_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;human_message_prompt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LLMChain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chat_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With that, we can pull everything together with the &lt;code&gt;reply_to_question_as&lt;/code&gt; function. It builds the chain, queries Chroma for relevant context, and then runs it:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# src/bot/replier/main.py&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;reply_to_question_as&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Replies to a question as a user (e.g., impersonates)&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_answering_chain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# using the callback to track cost&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;get_openai_callback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Answer from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;OpenAI callback: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cb&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;My friends really liked this one as well, and everyone cracked a laugh. But it was pretty obvious that the impersonation was not fooling anyone. It lacked &lt;em&gt;juice&lt;/em&gt;, one of my friends said.&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Both of these two bots were pretty fun to build, and &lt;a href="https://python-telegram-bot.org/"&gt;&lt;code&gt;python-telegram-bot&lt;/code&gt;&lt;/a&gt; makes building the whole thing so easy. It's basically a python script running &lt;a href="http://localhost:8000/blog/down-from-the-cloud-self-hosting.html"&gt;on my server&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The summarization feature was an instant success. It's simple, straightforward, and my friends loved it. The feedback to the impersonation feature was.. a bit of a mixed bag. Even though the model can accurately respond to a question with some relevant items to the person its impersonating, it's not really credible. It's missing the &lt;em&gt;juice&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;What is the &lt;em&gt;juice&lt;/em&gt;, you ask, my dear reader? The &lt;em&gt;juice&lt;/em&gt; is the voice of the person it's trying to impersonate. The &lt;em&gt;juice&lt;/em&gt; is the reason why when you ask ChatGPT to design something it looks pretty ugly. The &lt;em&gt;juice&lt;/em&gt; is the creativity, the originality!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(and of course, privacy concerns, here's my mention of them)&lt;/em&gt;&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Fine-tuning FLAN-T5 to replace my friends</title><link href="https://duarteocarmo.com/blog/fine-tune-flan-t5-telegram.html" rel="alternate"/><published>2023-05-24T16:15:00+02:00</published><updated>2023-05-24T16:15:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-05-24:/blog/fine-tune-flan-t5-telegram.html</id><summary type="html">&lt;p&gt;&lt;a href="https://github.com/duarteocarmo/fine-tune-flant5/blob/master/notebooks/t5_train.ipynb"&gt;View Code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;They say that the best way to learn about something is to build it yourself. Everyone talks about OpenAI this, and OpenAI that. How about we fine tune a Large Language Model ourselves?&lt;/p&gt;
&lt;p&gt;If you heard about this models before, but are still curious about how all of …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;a href="https://github.com/duarteocarmo/fine-tune-flant5/blob/master/notebooks/t5_train.ipynb"&gt;View Code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;They say that the best way to learn about something is to build it yourself. Everyone talks about OpenAI this, and OpenAI that. How about we fine tune a Large Language Model ourselves?&lt;/p&gt;
&lt;p&gt;If you heard about this models before, but are still curious about how all of these things work underneath, this blog post is just for you. With it, you'll learn how to fine tune a Large Language Model (Google's &lt;a href="https://huggingface.co/google/flan-t5-base"&gt;FLAN-T5&lt;/a&gt;) on the conversation history of a group chat you have lying around. The goal is to teach the model to talk exactly like your friends would. It won't be perfect, of course, but the goal here is to learn how these things work.&lt;/p&gt;
&lt;p&gt;Here's how we'll structure things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="#1-why-flan-t5"&gt;Why FLAN-T5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#2-setting-up-the-environment"&gt;Setting up your environment&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#3-data-preprocessing"&gt;Preparing the data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#4-fine-tuning-the-model"&gt;Fine-tuning the model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#5-generating-conversations"&gt;Generating conversations&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Even though I love taking credit for ideas that are not mine, this blog post was inspired by the work of some other awesome folks. Particularly &lt;a href="https://www.philschmid.de/fine-tune-flan-t5"&gt;this&lt;/a&gt; one, and &lt;a href="https://www.izzy.co/blogs/robo-boys.html"&gt;this&lt;/a&gt; one.&lt;/p&gt;
&lt;p&gt;Let's get started.&lt;/p&gt;
&lt;h2 id="1-why-flan-t5"&gt;1. Why FLAN-T5&lt;/h2&gt;
&lt;p&gt;There are a lot of Large Language Models (e.g., LLMs) out there. With the explosion of things like GPT-3 and GPT-4, lots of companies and open source organizations have started building their own LLMs. Some of these models &lt;a href="https://www.theverge.com/2023/3/8/23629362/meta-ai-language-model-llama-leak-online-misuse"&gt;leaked&lt;/a&gt;, others are &lt;a href="https://openai.com/"&gt;powerful&lt;/a&gt; but only available through an API.&lt;/p&gt;
&lt;p&gt;Some of theses LLMs that are actually free an open source, even for comercial use. Eugene Yan, has compiled a great &lt;a href="https://github.com/eugeneyan/open-llms"&gt;repo&lt;/a&gt; with an outline of some of these options.&lt;/p&gt;
&lt;p&gt;In 2020, Google released a paper called &lt;em&gt;&lt;a href="https://arxiv.org/pdf/1910.10683.pdf_"&gt;"Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer"&lt;/a&gt;&lt;/em&gt;, where they presented their T5 model. T5 is a encoder-decoder model that was trained in a variety of tasks (e.g., translate to german, summarize the following sentence, etc). &lt;a href="https://huggingface.co/docs/transformers/model_doc/flan-t5"&gt;FLAN-T5&lt;/a&gt; is basically the exact same thing as T5, but pretty much &lt;a href="https://huggingface.co/google/flan-t5-small#tldr"&gt;better at everything it does&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="FLAN-T5" src="https://camo.githubusercontent.com/623b4dea0b653f2ad3f36c71ebfe749a677ac0a1/68747470733a2f2f6d69726f2e6d656469756d2e636f6d2f6d61782f343030362f312a44304a31674e51663876727255704b657944387750412e706e67" /&gt;&lt;/p&gt;
&lt;p&gt;There are a couple of other reasons why we're using Flan-T5 for this guide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It's free, open source, and commercially available&lt;/li&gt;
&lt;li&gt;It has several &lt;a href="https://huggingface.co/docs/transformers/model_doc/flan-t5#overview"&gt;sizes&lt;/a&gt; we can use (from small, all the way to xxl)&lt;/li&gt;
&lt;li&gt;It's compatible with the whole &lt;a href="https://huggingface.co"&gt;Hugging Face&lt;/a&gt; 🤗 ecosystem, making our life easier&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With that. Let's get things started.&lt;/p&gt;
&lt;h2 id="2-setting-up-the-environment"&gt;2. Setting up the environment&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Note: This tutorial was run on a NVIDIA A100 with 40GB of RAM&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you want to run this project but don't have a powerful GPU at hand, you can get started quickly using &lt;a href="https://docs.unweave.io/docs/getting-started"&gt;Unweave&lt;/a&gt;. After signing up and installing, you can launch this project in two steps:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;#&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;unweave&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;

&lt;span class="sx"&gt;!unweave link your-username/fine-tune-llm&lt;/span&gt;

#&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;launch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;machine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;powered&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GPU&lt;/span&gt;

&lt;span class="sx"&gt;!unweave code --new --type a100 --image pytorch/pytorch:2.0.0-cuda11.7-cudnn8-devel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Also, to save and load models, you'll need to have a &lt;a href="https://huggingface.co"&gt;Hugging Face&lt;/a&gt; account. Once you have that set up, create and copy a new token. Paste it somewhere, we'll need it for later.&lt;/p&gt;
&lt;p&gt;The following cell, will install all required python libraries, as well as some local packages we'll need. (e.g., git, git-lfs)&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="sx"&gt;!pip install pytesseract evaluate tqdm transformers datasets rouge-score accelerate nltk tensorboard jupyter-black py7zr --upgrade&lt;/span&gt;
&lt;span class="sx"&gt;!apt-get install git --yes&lt;/span&gt;
&lt;span class="sx"&gt;!apt-get install git-lfs --yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Take your Hugging Face token, and replace it with in the field below. This will log you into the Hugging Face hub, which we'll need to push and pull models.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="sx"&gt;!huggingface-cli login --token XXXXXXXXXXXXXXXX&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="3-data-preprocessing"&gt;3. Data preprocessing&lt;/h2&gt;
&lt;p&gt;Let' start by preprocessing our data for training. We'll start by defining some variables. Feel free to replace the location of your &lt;a href="https://www.maketecheasier.com/export-telegram-chat-history/"&gt;Telegram group chat export&lt;/a&gt; in the &lt;code&gt;TELEGRAM_EXPORT&lt;/code&gt; variable:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pandas&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;jupyter_black&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datetime&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datasets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;

&lt;span class="n"&gt;jupyter_black&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;TELEGRAM_EXPORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;anonym_telegram.json&amp;quot;&lt;/span&gt;  &lt;span class="c1"&gt;# anonymized for obvious reasons&lt;/span&gt;
&lt;span class="n"&gt;CONVERSATION_LIMIT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20_000&lt;/span&gt;  &lt;span class="c1"&gt;# limit number of messages&lt;/span&gt;
&lt;span class="n"&gt;TEST_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;  &lt;span class="c1"&gt;# % of test data&lt;/span&gt;
&lt;span class="n"&gt;IS_NEW_SESSION_CUTOFF_MINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="mi"&gt;120&lt;/span&gt;  &lt;span class="c1"&gt;# if the time between messages is more than this, it&amp;#39;s a new session&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's preprocess this into a dataframe:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# load data in&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TELEGRAM_EXPORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;r&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;messages&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# create a dataframe&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)[[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;from&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;

&lt;span class="c1"&gt;# filter empty messages&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;from&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isna&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isna&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# convert to datetime&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inplace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# sort by date&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CONVERSATION_LIMIT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# limit number of messages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, the goal is to get our data into a format where the model can understand the conversation, and respond to what has been going on. Something like:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;conversation&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Person X: Where were you yesterday?\nPerson Y: I was at home! You?&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Person X: Me too, but thought of going out.&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will allow the model to see a thread and respond in the most realistic manner possible to the conversation that was already going on.&lt;/p&gt;
&lt;p&gt;However, we also know that group chats are pretty async, so we don't necessarily want a "good morning" message to be a direct response to whatever happened last night.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# telegram exports have some artifacts, let&amp;#39;s clean them up&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;clean_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;

&lt;span class="c1"&gt;# clean up and rename columns&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;chat&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;telegram&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;from&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;sender&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;message_date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;text&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;inplace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# create new sessions&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;last_event&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;chat&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;message_date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;is_new_session&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;message_date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;last_event&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fillna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;IS_NEW_SESSION_CUTOFF_MINS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;IS_NEW_SESSION_CUTOFF_MINS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;chat_session_id&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort_values&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;chat&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;message_date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;groupby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;chat&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;is_new_session&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cumsum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now that we have everything setup, it's time to create a &lt;code&gt;conversation&lt;/code&gt; column, that has all the messages before a certain response:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;sess_dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;records&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sess_dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;cstring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sess_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;chat_session_id&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;chat_session_id&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sess_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sender&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sess_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;response&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;cstring&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;cstring&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sess_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sender&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sess_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;response&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;cstring&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
            &lt;span class="n"&gt;cstring&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cstring&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;# create the conversation column&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;conversation&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;

&lt;span class="c1"&gt;# create the response column&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Your dataframe shape is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;You have the following columns: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;, &amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Your dataframe shape is (20000, 8)&lt;/span&gt;

&lt;span class="c1"&gt;# You have the following columns: response, sender, message_date, chat, last_event, is_new_session, chat_session_id, conversation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's see what a single example looks like:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;314&lt;/span&gt;&lt;span class="p"&gt;)[[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;conversation&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;sender&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;records&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Conversation:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;conversation&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Response:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;response&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Conversation:&lt;/span&gt;

&lt;span class="c1"&gt;# Hugo Silva: Lembram se do meu amigo alex frances?&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: Claro ya&lt;/span&gt;

&lt;span class="c1"&gt;# Hugo Silva: Ele correu a maratona de paris outra vez..&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: E…&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: Ou é só isso?&lt;/span&gt;

&lt;span class="c1"&gt;# Hugo Silva: Numero 304 overall...&lt;/span&gt;

&lt;span class="c1"&gt;# Hugo Silva: Crl&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: Eia cum crlh&lt;/span&gt;

&lt;span class="c1"&gt;# Hugo Silva: Pa doente&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: 3’46. Que louco fds&lt;/span&gt;

&lt;span class="c1"&gt;#&lt;/span&gt;

&lt;span class="c1"&gt;# Response:&lt;/span&gt;

&lt;span class="c1"&gt;# Hugo Silva: Doente completo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Our final preprocessing step is to load our dataframe in the &lt;a href="https://huggingface.co/docs/datasets/index"&gt;Datasets&lt;/a&gt; format:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;cols_for_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;conversation&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cols_for_dataset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pandas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# DatasetDict({&lt;/span&gt;

&lt;span class="c1"&gt;#     train: Dataset({&lt;/span&gt;

&lt;span class="c1"&gt;#         features: [&amp;#39;conversation&amp;#39;, &amp;#39;response&amp;#39;, &amp;#39;__index_level_0__&amp;#39;],&lt;/span&gt;

&lt;span class="c1"&gt;#         num_rows: 16000&lt;/span&gt;

&lt;span class="c1"&gt;#     })&lt;/span&gt;

&lt;span class="c1"&gt;#     test: Dataset({&lt;/span&gt;

&lt;span class="c1"&gt;#         features: [&amp;#39;conversation&amp;#39;, &amp;#39;response&amp;#39;, &amp;#39;__index_level_0__&amp;#39;],&lt;/span&gt;

&lt;span class="c1"&gt;#         num_rows: 4000&lt;/span&gt;

&lt;span class="c1"&gt;#     })&lt;/span&gt;

&lt;span class="c1"&gt;# })&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Great. We're ready to focus on the model.&lt;/p&gt;
&lt;h2 id="4-fine-tuning-the-model"&gt;4. Fine-tuning the model&lt;/h2&gt;
&lt;p&gt;We start with some training specific imports, mostly related to Hugging Face.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;transformers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AutoModelForSeq2SeqLM&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;datasets&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;concatenate_datasets&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;evaluate&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;nltk&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;numpy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;nltk.tokenize&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sent_tokenize&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;transformers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DataCollatorForSeq2Seq&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;huggingface_hub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HfFolder&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;transformers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;random&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;randrange&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;transformers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Seq2SeqTrainer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Seq2SeqTrainingArguments&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;os&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, we define the most important variables for training, make sure to read the description of each one:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;MODEL_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;chat&amp;quot;&lt;/span&gt;  &lt;span class="c1"&gt;# the name of your model&lt;/span&gt;
&lt;span class="n"&gt;MODEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;google/flan-t5-small&amp;quot;&lt;/span&gt;  &lt;span class="c1"&gt;# the id of the base model we will train (can be small, base, large, xl, etc.) (the bigger - the more GPU memory you need)&lt;/span&gt;
&lt;span class="n"&gt;REPOSITORY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODEL_ID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODEL_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;  &lt;span class="c1"&gt;# the id of your huggingface repository where the model will be stored&lt;/span&gt;
&lt;span class="n"&gt;NUM_TRAIN_EPOCHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;  &lt;span class="c1"&gt;# number of epochs to train&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's load the model and the tokenizer with the help of &lt;code&gt;AutoModelForSeq2SeqLM&lt;/code&gt;:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForSeq2SeqLM&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MODEL_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MODEL_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Some samples in our dataset will likely be too long for our model. So they'll need to be truncated. In the cell below, we define the max conversation and response lenght. This will then help in the truncating process:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# source&lt;/span&gt;

&lt;span class="n"&gt;tokenized_inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;concatenate_datasets&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;train&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;test&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;conversation&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;truncation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;batched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;max_source_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tokenized_inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;input_ids&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;

&lt;span class="c1"&gt;# target&lt;/span&gt;

&lt;span class="n"&gt;tokenized_targets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;concatenate_datasets&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;train&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;test&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;truncation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;batched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;max_target_length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tokenized_targets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;input_ids&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Max source length: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_source_length&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Max target length: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_target_length&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now that we know the limit for the truncation, we'll preprocess our dataset to be fed into the model. During its original training, FLAN T5 used &lt;a href="https://github.com/google-research/FLAN/blob/main/flan/v2/flan_templates_branched.py"&gt;different templates&lt;/a&gt; for training on multiple tasks. Here, I decided to use the &lt;code&gt;Continue writing the following text&lt;/code&gt; template. I did not test all of them, so feel free to modify according to what suits your task best.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;preprocess_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;max_length&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;template_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Continue writing the following text.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;template_start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;conversation&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;

    &lt;span class="n"&gt;model_inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_source_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truncation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;text_target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_target_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;truncation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;padding&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;max_length&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;input_ids&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pad_token_id&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;input_ids&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;model_inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;labels&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;input_ids&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;model_inputs&lt;/span&gt;

&lt;span class="n"&gt;tokenized_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;preprocess_function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remove_columns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;conversation&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;response&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Keys of tokenized dataset: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenized_dataset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;train&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's define some metrics for the evaluation of the model:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# metric&lt;/span&gt;

&lt;span class="n"&gt;metric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evaluate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;rouge&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;nltk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;download&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;punkt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# postprocess text&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;postprocess_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;preds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;preds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sent_tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sent_tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;

&lt;span class="c1"&gt;# compute metrics&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;compute_metrics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eval_preds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eval_preds&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;preds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;decoded_preds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batch_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;preds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skip_special_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Replace -100 in the labels as we can&amp;#39;t decode them.&lt;/span&gt;
    &lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pad_token_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;decoded_labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batch_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skip_special_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Some simple post-processing&lt;/span&gt;
    &lt;span class="n"&gt;decoded_preds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decoded_labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;postprocess_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decoded_preds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decoded_labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;decoded_preds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;references&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;decoded_labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;use_stemmer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;
    &lt;span class="n"&gt;prediction_lens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count_nonzero&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pad_token_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;preds&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gen_len&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prediction_lens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Our final data preparation step is to use &lt;code&gt;DataCollatorForSeq2Seq&lt;/code&gt; to handle the padding for inputs and labels:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;label_pad_token_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="n"&gt;data_collator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DataCollatorForSeq2Seq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label_pad_token_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;label_pad_token_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pad_to_multiple_of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, we can define the training arguments. Feel free to play around with these. Depending on the use case, results can vary!&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Define training args&lt;/span&gt;

&lt;span class="n"&gt;training_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Seq2SeqTrainingArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;# training parameters&lt;/span&gt;
    &lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REPOSITORY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;per_device_train_batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;per_device_eval_batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;predict_with_generate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;fp16&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Overflows with fp16&lt;/span&gt;
    &lt;span class="n"&gt;learning_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5e-5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;num_train_epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NUM_TRAIN_EPOCHS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# logging &amp;amp; evaluation strategies&lt;/span&gt;
    &lt;span class="n"&gt;logging_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;REPOSITORY_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/logs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;logging_strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;steps&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;logging_steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;evaluation_strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;epoch&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;save_strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;epoch&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;save_total_limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;load_best_model_at_end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# push to hub parameters&lt;/span&gt;
    &lt;span class="n"&gt;report_to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tensorboard&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;push_to_hub&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hub_strategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;every_save&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hub_model_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REPOSITORY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hub_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HfFolder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_token&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;disable_tqdm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Create Trainer instance&lt;/span&gt;

&lt;span class="n"&gt;trainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Seq2SeqTrainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;training_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;data_collator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data_collator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;train_dataset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tokenized_dataset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;train&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;eval_dataset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tokenized_dataset&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;test&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;compute_metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;compute_metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, we can kick-off training! This can take ~20 mins if you use the small model, or about a couple of hours with the base model. Of course, this largely depends on how much data you have, what's your GPU, epochs, etc. Feel free to tweak the params above to your liking!&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Start training&lt;/span&gt;

&lt;span class="n"&gt;trainer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once training is done, we can now push things to the hub!&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REPOSITORY_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;trainer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_model_card&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;trainer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push_to_hub&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push_to_hub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REPOSITORY_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="5-generating-conversations"&gt;5. Generating conversations&lt;/h2&gt;
&lt;p&gt;Now, generation with these models can be quite a challenge. OpenAI for example, used &lt;a href="https://openai.com/research/instruction-following"&gt;RLHF&lt;/a&gt; to align these models, and reward them for generating nice outputs. Here's a great sketch that illustrates the importance of the different training steps:&lt;/p&gt;
&lt;p&gt;&lt;img alt="image" src="https://wompampsupport.azureedge.net/fetchimage?siteId=7575&amp;amp;v=2&amp;amp;jpgQuality=100&amp;amp;width=700&amp;amp;url=https%3A%2F%2Fi.kym-cdn.com%2Fentries%2Ficons%2Ffacebook%2F000%2F044%2F025%2Fshoggothhh_header.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;In our use case, there are some parameters we can leverage when generating text. In &lt;a href="https://huggingface.co/blog/how-to-generate"&gt;this article&lt;/a&gt;, Patrick Von Platen goes through the different techniques &amp;amp; methods we can use to control the output of these models.&lt;/p&gt;
&lt;p&gt;There are &lt;em&gt;quite a few&lt;/em&gt; parameters you can tweak:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Either you are using greedy or beam search&lt;/li&gt;
&lt;li&gt;Sampling&lt;/li&gt;
&lt;li&gt;Top-K sampling&lt;/li&gt;
&lt;li&gt;Top-P sampling&lt;/li&gt;
&lt;li&gt;Size of n-grams to not repeat&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I advise you to go through the article and learn a bit about each one of these!&lt;/p&gt;
&lt;p&gt;Context given, let's get to it:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's first load the model we pushed to the hub. (You can also just use the directory where your model is saved instead.)&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# load tokenizer and model&lt;/span&gt;

&lt;span class="n"&gt;REPOSITORY_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;flan-t5-small-chat/checkpoint-8000/&amp;quot;&lt;/span&gt;  &lt;span class="c1"&gt;# the name of your repository where the model was pushed&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoTokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REPOSITORY_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AutoModelForSeq2SeqLM&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_pretrained&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REPOSITORY_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# put model on GPU&lt;/span&gt;

&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;cuda:0&amp;quot;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;cpu&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To kick off the generation, we select a &lt;em&gt;random&lt;/em&gt; converstaion in our dataset:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;test&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;STARTING_TEXT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;conversation&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STARTING_TEXT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: 12h EM PORTUGAL:&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: 2 votos por pessoa pfv.&lt;/span&gt;

&lt;span class="c1"&gt;# Hugo Silva: Nao é anonymous?&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: Não. Tudo visivel&lt;/span&gt;

&lt;span class="c1"&gt;# Luís Rodrigues: Foi uma bela administração sim senhor.&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: Obrigado a esta administração! Dinâmica, Pacífica, Estruturadora, Libertadora!&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: #XeJonnyLegislativas2026&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: (2026 right? 😅)&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: PORTUGAL HOJE&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Ahahaha vamos!!!!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# remember to learn and tweak these params&lt;/span&gt;

&lt;span class="n"&gt;generation_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;max_length&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;no_repeat_ngram_size&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;do_sample&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;top_k&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;top_p&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;temperature&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;num_return_sequences&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;repetition_penalty&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;encoded_conversation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STARTING_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;output_encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoded_conversation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;generation_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;output_decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_encoded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;skip_special_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Response:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;output_decoded&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Response:&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Mas é uma bela&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Another fun thing to do, is to let the model generate conversations completely by himself.&lt;/p&gt;
&lt;p&gt;The idea here is to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start with a real conversation (5 replies)&lt;/li&gt;
&lt;li&gt;Generate a response using our model&lt;/li&gt;
&lt;li&gt;Create a new exchange (4 real replies + 1 AI generated)&lt;/li&gt;
&lt;li&gt;Generate a response&lt;/li&gt;
&lt;li&gt;Create a new exchange (3 real replies + 2 AI generated)&lt;/li&gt;
&lt;li&gt;etc..&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Eventually, we'll end up with a bunch of AI generated conversations from our model! Here's the code to do that:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;conversation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;NUMBER_OF_ROUNDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NUMBER_OF_ROUNDS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conversation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;STARTING_TEXT&lt;/span&gt;

    &lt;span class="n"&gt;encoded_conversation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_tensors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_ids&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;device&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;output_encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoded_conversation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;generation_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;output_decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_encoded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;skip_special_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;conversation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conversation&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;output_decoded&lt;/span&gt;
    &lt;span class="n"&gt;conversation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;  &lt;span class="c1"&gt;# remove first intervention&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;New conversation:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;----&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# New conversation:&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: 2 votos por pessoa pfv.&lt;/span&gt;

&lt;span class="c1"&gt;# Hugo Silva: Nao é anonymous?&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: Não. Tudo visivel&lt;/span&gt;

&lt;span class="c1"&gt;# Luís Rodrigues: Foi uma bela administração sim senhor.&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: Obrigado a esta administração! Dinâmica, Pacífica, Estruturadora, Libertadora!&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: #XeJonnyLegislativas2026&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: (2026 right? 😅)&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: PORTUGAL HOJE&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Ahahaha vamos!!!!&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Ahaha&lt;/span&gt;

&lt;span class="c1"&gt;# ----&lt;/span&gt;

&lt;span class="c1"&gt;# New conversation:&lt;/span&gt;

&lt;span class="c1"&gt;# Hugo Silva: Nao é anonymous?&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: Não. Tudo visivel&lt;/span&gt;

&lt;span class="c1"&gt;# Luís Rodrigues: Foi uma bela administração sim senhor.&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: Obrigado a esta administração! Dinâmica, Pacífica, Estruturadora, Libertadora!&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: #XeJonnyLegislativas2026&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: (2026 right? 😅)&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: PORTUGAL HOJE&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Ahahaha vamos!!!!&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Ahaha&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: Pa no estou?&lt;/span&gt;

&lt;span class="c1"&gt;# ----&lt;/span&gt;

&lt;span class="c1"&gt;# New conversation:&lt;/span&gt;

&lt;span class="c1"&gt;# Tiago Pereira: Não. Tudo visivel&lt;/span&gt;

&lt;span class="c1"&gt;# Luís Rodrigues: Foi uma bela administração sim senhor.&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: Obrigado a esta administração! Dinâmica, Pacífica, Estruturadora, Libertadora!&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: #XeJonnyLegislativas2026&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: (2026 right? 😅)&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: PORTUGAL HOJE&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Ahahaha vamos!!!!&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Ahaha&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: Pa no estou?&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: No estou a caralhar&lt;/span&gt;

&lt;span class="c1"&gt;# ----&lt;/span&gt;

&lt;span class="c1"&gt;# New conversation:&lt;/span&gt;

&lt;span class="c1"&gt;# Luís Rodrigues: Foi uma bela administração sim senhor.&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: Obrigado a esta administração! Dinâmica, Pacífica, Estruturadora, Libertadora!&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: #XeJonnyLegislativas2026&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: (2026 right? 😅)&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: PORTUGAL HOJE&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Ahahaha vamos!!!!&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Ahaha&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: Pa no estou?&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: No estou a caralhar&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: Ainda no estou?&lt;/span&gt;

&lt;span class="c1"&gt;# ----&lt;/span&gt;

&lt;span class="c1"&gt;# New conversation:&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: Obrigado a esta administração! Dinâmica, Pacífica, Estruturadora, Libertadora!&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: #XeJonnyLegislativas2026&lt;/span&gt;

&lt;span class="c1"&gt;# Leonardo Soares: (2026 right? 😅)&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: PORTUGAL HOJE&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Ahahaha vamos!!!!&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Ahaha&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: Pa no estou?&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: No estou a caralhar&lt;/span&gt;

&lt;span class="c1"&gt;# André Ferreira: Ainda no estou?&lt;/span&gt;

&lt;span class="c1"&gt;# Raul Carvalho: Haha nvel queres meeses&lt;/span&gt;

&lt;span class="c1"&gt;# ----&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="blog"/></entry><entry><title>Governo Sombra transcripts</title><link href="https://duarteocarmo.com/blog/governo-sombra-transcripts.html" rel="alternate"/><published>2023-04-19T16:30:00+02:00</published><updated>2023-04-19T16:30:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-04-19:/blog/governo-sombra-transcripts.html</id><summary type="html">&lt;center&gt;
&lt;a href="https://governosombra.duarteocarmo.com" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/51/website.png" alt="governosombra.duarteocarmo.com" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;7 years. That's how long I've lived in Denmark for. I love it, but Portugal is still close to my heart. As an emigrant, it's always hard to stay connected to what's going on in Portugal. What are people talking about? What's in the news? What worries people? What is …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;a href="https://governosombra.duarteocarmo.com" target="_blank"&gt;
&lt;img src="https://duarteocarmo.com/images/51/website.png" alt="governosombra.duarteocarmo.com" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;
&lt;/center&gt;

&lt;p&gt;7 years. That's how long I've lived in Denmark for. I love it, but Portugal is still close to my heart. As an emigrant, it's always hard to stay connected to what's going on in Portugal. What are people talking about? What's in the news? What worries people? What is everyone arguing about over morning coffee?&lt;/p&gt;
&lt;p&gt;One of the ways I like to stay in touch is by listening to &lt;em&gt;Governo Sombra&lt;/em&gt; (now cleverly called  &lt;em&gt;Program whose name we are legally prevented from saying&lt;/em&gt;, after changing networks). It's a weekly show where the 3 guests (+1 host) comment on Portuguese and World news. Besides being funny, I also love the fact that the 3 guests represent different parts of the political spectrum, so I can get a good idea about how most of the people are feeling.&lt;/p&gt;
&lt;p&gt;Inspired by &lt;a href="https://karpathy.ai/lexicap/"&gt;Lexicap&lt;/a&gt;, I decided to build a &lt;a href="https://governosombra.duarteocarmo.com"&gt;website&lt;/a&gt; with the transcripts for all of the episodes of the show. More than once I've listened to a particular part of an episode and wanted to share it with a friend. Now, &lt;a href="https://governosombra.duarteocarmo.com/episodes/171#170"&gt;I can do it&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For the transcription, I used OpenAI's Open Source &lt;a href="https://github.com/openai/whisper"&gt;Whisper&lt;/a&gt; model. With a  &lt;em&gt;small&lt;/em&gt; caveat: the whole thing (serving + transcribing) needed to run in my 20 EUR/month &lt;a href="/blog/down-from-the-cloud-self-hosting.html"&gt;VM&lt;/a&gt;. So it needed to be small &lt;em&gt;and&lt;/em&gt; efficient.&lt;/p&gt;
&lt;p&gt;I like Python, but Rust was the obvious choice. For the transcription part, I used &lt;a href="https://github.com/tazz4843/whisper-rs"&gt;whisper.rs&lt;/a&gt; (Rust bindings for &lt;a href="https://github.com/ggerganov/whisper.cpp/"&gt;whisper.cpp&lt;/a&gt;). For serving the app, I went with &lt;a href="https://actix.rs/"&gt;Actix Web&lt;/a&gt; - it's small, efficient, and reminds me a lot of Flask. Incredible how a small Linux box can handle transcribing 60min+ episodes without hiccuping much.&lt;/p&gt;
&lt;p&gt;The quality of the transcription is something like a 6/10. I did use the &lt;a href="https://github.com/openai/whisper#available-models-and-languages"&gt;base&lt;/a&gt; model so there is &lt;em&gt;clearly&lt;/em&gt; space for improvement. Maybe when I get a dedicated box.&lt;/p&gt;
&lt;p&gt;The entire thing is up on &lt;a href="https://github.com/duarteocarmo/governosombra"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>LLMs in production: lessons learned</title><link href="https://duarteocarmo.com/blog/llms-lessons-learned.html" rel="alternate"/><published>2023-03-26T20:35:00+02:00</published><updated>2023-03-26T20:35:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-03-26:/blog/llms-lessons-learned.html</id><summary type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/50/openai_3.png" alt="OpenAI image 1" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;A couple of months ago nobody asked me about my work. &lt;em&gt;Something&lt;/em&gt; related to computers and AI. Fast forward to today, even my uncle asks me about ChatGPT. The &lt;a href="https://www.urbandictionary.com/define.php?term=hype"&gt;&lt;em&gt;hype&lt;/em&gt;&lt;/a&gt; is real. Only time will tell if the hype will materialize. But while the world wonders, work goes on.&lt;/p&gt;
&lt;p&gt;In …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/50/openai_3.png" alt="OpenAI image 1" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;A couple of months ago nobody asked me about my work. &lt;em&gt;Something&lt;/em&gt; related to computers and AI. Fast forward to today, even my uncle asks me about ChatGPT. The &lt;a href="https://www.urbandictionary.com/define.php?term=hype"&gt;&lt;em&gt;hype&lt;/em&gt;&lt;/a&gt; is real. Only time will tell if the hype will materialize. But while the world wonders, work goes on.&lt;/p&gt;
&lt;p&gt;In the last couple of months, I've helped develop a product that leverages this tech at its core. It was - to say the least - a learning experience. Full of lessons learned, full of little traumas and things I would've done better. In the hopes of helping someone out there, here are some lessons I've learned along the way.&lt;/p&gt;
&lt;h3 id="know-your-use-case"&gt;Know your use case&lt;/h3&gt;
&lt;p&gt;With so much hype surrounding LLMs, it's easy to think they'll solve all problems in Machine Learning. Or at least the ones related to NLP. From what I've seen, this is hardly the case. Let's split Machine Learning tasks into two types. (1) Predictive tasks, such as classifying the sentiment of a tweet, and (2) generative tasks, such as summarizing the content of an article.&lt;/p&gt;
&lt;p&gt;GPT is &lt;em&gt;great&lt;/em&gt; at generative tasks. Writing an email with context, writing a summary of a web page, and creating an article given some ideas. These are a very specific subset of Machine Learning. Most of the problems we face are predictive problems: what is the sentiment of this tweet? What is the class of this image? It's hard to tell exactly how good these models will become for the predictive use case. But before throwing GPT at whatever you're facing, think about the use case. This leads me to my next point.&lt;/p&gt;
&lt;h3 id="deterministic-vs-stochastic"&gt;Deterministic vs. stochastic&lt;/h3&gt;
&lt;p&gt;Do you know the classic "&lt;em&gt;I cannot reproduce this issue&lt;/em&gt;" we've said oh so many times? Well, welcome to a whole other level of that. "&lt;em&gt;The AI said something wrong&lt;/em&gt;" is a very common issue I've faced with these models. At the core, LLMs are stochastic, and not deterministic. Before LLMs, whatever model we were building, given an input, would always produce the same output. With LLMs, given the same input, the output is &lt;em&gt;rarely&lt;/em&gt; the same.&lt;/p&gt;
&lt;p&gt;This is amazing for generative tasks but can become a real problem for predictive tasks. The problem of reproducibility. To avoid this issue, you can play around with some of the parameters of this model such as the &lt;a href="https://lukesalamone.github.io/posts/what-is-temperature/"&gt;temperature&lt;/a&gt; or the presence penalty. This further reinforces the idea that these models are great for generating text - where the cost of failure is low. If you're predicting something with a high cost of failure - best beware.&lt;/p&gt;
&lt;h3 id="prepare-for-the-future"&gt;Prepare for the future&lt;/h3&gt;
&lt;p&gt;This tech is moving incredibly fast. Yes, even for &lt;em&gt;us&lt;/em&gt;, the group of people that &lt;em&gt;loves&lt;/em&gt; to move fast. OpenAI, is releasing models and updates at an astronomical pace. By the time you finished developing that shiny new product, there will probably be a new one. This happened to us actually. We started developing a system based on GPT-3 (e.g., &lt;code&gt;text-davinci-003&lt;/code&gt;).  Shortly after, OpenAI released the ChatGPT model (e.g., &lt;code&gt;gpt-3.5-turbo&lt;/code&gt;). Two days before we went to production - GPT-4 (e.g., &lt;code&gt;gpt-4&lt;/code&gt;) was here.&lt;/p&gt;
&lt;p&gt;These models use different APIs, structures, and behaviors. Thankfully, I'd spent a Friday afternoon implementing the &lt;a href="https://refactoring.guru/design-patterns/strategy/python/example"&gt;Strategy Pattern&lt;/a&gt;, so that we could support both &lt;a href="https://platform.openai.com/docs/guides/completion"&gt;Text Completion&lt;/a&gt; and &lt;a href="https://platform.openai.com/docs/guides/chat"&gt;Chat Completion&lt;/a&gt; for our system. Turned out to be time well spent, and adopting GPT-4 was a moderate one-line code change. Whenever a new model comes out, expect to be rate limited, and expect timeouts. Especially with these technologies, it's worth preparing for the future, and anticipating what &lt;em&gt;might&lt;/em&gt; be released. Without &lt;a href="/blog/simple-software.html"&gt;exaggerating&lt;/a&gt;, of course.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/50/openai_2.png" alt="OpenAI image 2" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;h3 id="streaming-vs-batching"&gt;Streaming vs. Batching&lt;/h3&gt;
&lt;p&gt;Yes, it comes down to that, a single API call to OpenAI. (Well, at least while we don't have a solid open-source alternative). Given a large enough prompt, these models can take quite a while to return a result. There's nothing worse than making a user wait for 20-30 seconds before showing some action on the screen - especially when that request ends up failing sometimes. After some days of frustration, a co-worker sent me &lt;a href="https://github.com/openai/openai-cookbook/blob/main/examples/How_to_stream_completions.ipynb"&gt;this&lt;/a&gt; link.&lt;/p&gt;
&lt;p&gt;Instead of waiting for the whole completion to be finished, consider streaming the response. In short, it allows you to start receiving completion tokens as soon as they're generated. Together with FastAPI's excellent implementation of &lt;a href="https://fastapi.tiangolo.com/advanced/custom-response/#streamingresponse"&gt;&lt;code&gt;StreamingResponse&lt;/code&gt;&lt;/a&gt;, it allows you to show the completion happening in real-time to users. This pattern is widely used by implementations such as &lt;a href="https://www.notion.so/product/ai"&gt;Notion's&lt;/a&gt;. Now that all we have is an API, customer experience is even more differentiating.&lt;/p&gt;
&lt;h3 id="prompt-engineering-is-hard"&gt;Prompt engineering is hard&lt;/h3&gt;
&lt;p&gt;We have prompt engineers now. No wonder. I prefer the term prompt &lt;em&gt;artists&lt;/em&gt;. As soon as we had to put a system in place we struggled. We are used to fancy tools to track every parameter of an experiment. Now that we're designing prompts for a stochastic system the game has changed. How can know the impact of every little change in the prompt? How can I know how one instruction affects another instruction? What really defines a &lt;em&gt;good&lt;/em&gt; prompt? You see where I'm getting at. For now, it's an art - more than engineering.&lt;/p&gt;
&lt;p&gt;Very soon, our prompt was a list of 10-15 instructions giving very specific directions on what we needed to generate. To battle this, we created a set of scripts that would allow us to quickly compare the impact of small changes in the prompt. This is a costly exercise - given a stochastic system - since we have to call a paid API 2/3x to compare the effect of prompt changes. This allowed us to at least get a decent direction on how good the prompt is. Still, I feel like prompt engineering is still a lot of shooting in the dark.&lt;/p&gt;
&lt;h3 id="conclusion-not-the-hammer-you-were-looking-for"&gt;Conclusion: Not the hammer you were looking for&lt;/h3&gt;
&lt;p&gt;LLMs are incredibly powerful tools. From generating text to now even supporting images - it's hard to tell where the field will lead us. For creative and generative tasks they shine like nothing before in our field. Being able to have access to such a powerful system will be groundbreaking in a lot of fields of machine learning. But with great power comes great responsibility - or &lt;em&gt;baggage&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Putting an LLM into production is a challenging problem with a lot of unknowns. From depending on a simple API call, to not being able to reproduce results - it can get complicated.&lt;/p&gt;
&lt;p&gt;If the task is simple enough (e.g., classification) it's still hard to justify a more expensive, less explainable, and albeit slower system than &lt;em&gt;traditional&lt;/em&gt; methods. I don't think this is the hammer for all our problems. At least not &lt;em&gt;yet&lt;/em&gt;.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>parlabot - ask the Portuguese parliament</title><link href="https://duarteocarmo.com/blog/parlabot.html" rel="alternate"/><published>2023-02-27T18:55:00+01:00</published><updated>2023-02-27T18:55:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-02-27:/blog/parlabot.html</id><summary type="html">&lt;p&gt;Large language models (LLM) are really &lt;a href="https://www.theverge.com/2023/2/8/23590864/google-ai-chatbot-bard-mistake-error-exoplanet-demo"&gt;dumb&lt;/a&gt;. I mean, how can you fail when the question is as simple as "&lt;em&gt;What is 23 times 18&lt;/em&gt;"? Even though they're making most headlines, at the end of the day, these models are, predicting the next token based on the previous ones. If …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Large language models (LLM) are really &lt;a href="https://www.theverge.com/2023/2/8/23590864/google-ai-chatbot-bard-mistake-error-exoplanet-demo"&gt;dumb&lt;/a&gt;. I mean, how can you fail when the question is as simple as "&lt;em&gt;What is 23 times 18&lt;/em&gt;"? Even though they're making most headlines, at the end of the day, these models are, predicting the next token based on the previous ones. If we ask a super simple question without any context to an LLM, the performance will only depend on &lt;a href="https://arxiv.org/pdf/2202.07206.pdf"&gt;how much the model has seen that example&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But LLMs are also amazing. Have you used &lt;a href="https://github.com/features/copilot"&gt;GitHub copilot&lt;/a&gt; lately? I don't trust it to write my functions for me, but damn it writes a good boilerplate. It's like auto-complete, but &lt;em&gt;better&lt;/em&gt;. It doesn't only know your imports. It knows where you are in the code base, and can suggest based on that.&lt;/p&gt;
&lt;p&gt;The difference? &lt;em&gt;Context&lt;/em&gt;. When using copilot, your code &lt;em&gt;is&lt;/em&gt; the context, and that's why copilot knows exactly what to suggest. So how can I give the right context to one of these models? To answer this, I built &lt;a href="https://parlabot.duarteocarmo.com/"&gt;parlabot&lt;/a&gt;. Parlabot (&lt;a href="https://www.collinsdictionary.com/dictionary/portuguese-english/parlamento"&gt;parlamento&lt;/a&gt; + bot) uses all transcripts from the Portuguese parliament to answer any question you might have about Portuguese politics. It also (tries) to do so in the most truthful way it can, with the information it has.&lt;/p&gt;
&lt;p&gt;&lt;a target="_blank" href="https://parlabot.duarteocarmo.com"&gt;
&lt;img src="https://duarteocarmo.com/images/49/parlabot-screenshot.png" alt="Parlabot website" style="max-width:100%;"&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;There are two things at play when you ask a question to parlabot. Below is a sketch that might make things easier to follow.&lt;/p&gt;
&lt;p&gt;The first part is the search engine. To make this work, I scrapped all transcripts from the Portuguese parliament and used a multilingual embedding model to transform them into vectors. Whenever you type in a question, I embed it using the same exact model, transforming it into another vector. I then find the top k most similar reference vectors to the query vector. This is the same as finding the top K most relevant speech segments for that question.&lt;/p&gt;
&lt;p&gt;Once I have the most relevant speech segments, it's time to build the &lt;em&gt;prompt&lt;/em&gt;. The prompt, has two parts, the &lt;em&gt;context&lt;/em&gt;, and the &lt;em&gt;direction&lt;/em&gt;. The context, is simply the most relevant speech segments, the corresponding speakers, and the political parties. In the direction part of the prompt, I ask GPT3 to answer a given question using the segments from the context.&lt;/p&gt;
&lt;p&gt;&lt;a target="_blank" href="https://parlabot.duarteocarmo.com"&gt;
&lt;img src="https://duarteocarmo.com/images/49/LLM-sketch.png" alt="LLM Question/Answer system" style="max-width:100%;border-radius: 2px"&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The results are pretty good! The bot is very capable of using the context given to answer most questions. Of course, the context is not always relevant to a question. For example, if you ask "How to bake a cake?", it's unlikely we'll find the answer in the data set of parliament transcriptions. With good prompt design, we can make the bot answer "&lt;em&gt;I don't know&lt;/em&gt;" when this is the case.&lt;/p&gt;
&lt;p&gt;The mix of LLMs, politics, and poorly written code is a great recipe for disaster. Although disaster does sound pretty fun,  I don't expect this bot to answer &lt;em&gt;truthfully&lt;/em&gt; any of the questions Portuguese tax-payers have about elected parties. These answers should be taken with a massive grain of salt and skepticism. LLMs are stochastic beasts that might predict two different outputs with the exact same set of inputs.&lt;/p&gt;
&lt;p&gt;Still, this system might be interesting to solve problems where the cost of failure is not catastrophic. Without forgetting, that a bit of skepticism is a great foundation for any ML system.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>An opinionated Python boilerplate</title><link href="https://duarteocarmo.com/blog/opinionated-python-boilerplate.html" rel="alternate"/><published>2023-02-18T00:00:00+01:00</published><updated>2023-02-18T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-02-18:/blog/opinionated-python-boilerplate.html</id><summary type="html">&lt;p&gt;There's nothing quite like starting a new project. A greenfield, &lt;em&gt;filled&lt;/em&gt; with possibilities. It's a privilege many don't come across. A lot of us, get thrown into projects with a lot of legacy code. But sometimes, we start from scratch.&lt;/p&gt;
&lt;p&gt;This is the time. The time to make all the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;There's nothing quite like starting a new project. A greenfield, &lt;em&gt;filled&lt;/em&gt; with possibilities. It's a privilege many don't come across. A lot of us, get thrown into projects with a lot of legacy code. But sometimes, we start from scratch.&lt;/p&gt;
&lt;p&gt;This is the time. The time to make all the right calls. The time to use the right tools, the right abstractions, and the right structure. The unfortunate truth is, &lt;a href="/blog/simple-software.html"&gt;there is no &lt;em&gt;right&lt;/em&gt; way&lt;/a&gt;. There are just &lt;em&gt;ways.&lt;/em&gt; Most of us use the tools we're comfortable with. We've tried things in the past. Some worked, some didn't.&lt;/p&gt;
&lt;p&gt;I've started my fair share of Python projects. By failing, &lt;em&gt;a lot&lt;/em&gt;, I've converged to a set of tools. These will change over time. But in the hope to help a fellow Pythonista out there, let's talk about them.&lt;/p&gt;
&lt;h2 id="pip-tools-for-dependency-management"&gt;&lt;a href="https://pip-tools.readthedocs.io/en/latest/"&gt;Pip-tools&lt;/a&gt; for dependency management&lt;/h2&gt;
&lt;p&gt;Now I don't want to start a packaging war. If &lt;a href="https://python-poetry.org/"&gt;Poetry&lt;/a&gt; works for you, then by all means, go for it. I've tried most of the stuff out there, from the good old &lt;code&gt;pip freeze &amp;gt; requirements.txt&lt;/code&gt;, &lt;a href="https://pipenv.pypa.io/en/latest/"&gt;pipenv&lt;/a&gt;, all the way to Poetry. After many battles, I've stuck with &lt;a href="https://github.com/jazzband/pip-tools"&gt;pip-tools&lt;/a&gt;. Pip-tools strikes the right balance between simplicity, effectiveness, and speed. And yes, &lt;em&gt;speed&lt;/em&gt; matters. I don't want to wait a whole minute for my dependencies to compile.&lt;/p&gt;
&lt;p&gt;With recent pip updates, I can just specify my dependencies in a &lt;code&gt;pyproject.toml&lt;/code&gt; and install them with &lt;code&gt;pip install -e .&lt;/code&gt;. However, there are benefits to pinning your dependencies using something like &lt;a href="https://github.com/jazzband/pip-tools#pip-tools--pip-compile--pip-sync"&gt;pip-tools&lt;/a&gt;. Especially in Machine Learning environments. Yes Anaconda exists - but speed &lt;em&gt;is&lt;/em&gt; a requirement.&lt;/p&gt;
&lt;p&gt;The folks at &lt;a href="https://jazzband.co/"&gt;Jazzband&lt;/a&gt; have created an easy-to-use tool that has yet to let me down. For example, suppose I have a &lt;code&gt;dreambox&lt;/code&gt; project. Here's an example &lt;code&gt;pyproject.toml&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[build-system]&lt;/span&gt;
&lt;span class="n"&gt;requires&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;hatchling&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;build-backend&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="s2"&gt;&amp;quot;hatchling.build&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[project]&lt;/span&gt;
&lt;span class="n"&gt;name&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="s2"&gt;&amp;quot;dreambox&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;version&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="s2"&gt;&amp;quot;42&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;dependencies&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pandas&amp;gt;=1.5.3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;numpy&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fastapi&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;[project.optional-dependencies]&lt;/span&gt;
&lt;span class="n"&gt;dev&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pytest&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To create a pinned and hashed &lt;code&gt;requirements&lt;/code&gt; file , all you need to do is:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;env&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;pip-compile&lt;span class="w"&gt; &lt;/span&gt;--generate-hashes&lt;span class="w"&gt; &lt;/span&gt;--output-file&lt;span class="o"&gt;=&lt;/span&gt;requirements.txt&lt;span class="w"&gt; &lt;/span&gt;pyproject.toml
&lt;span class="o"&gt;(&lt;/span&gt;env&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;$&lt;span class="w"&gt; &lt;/span&gt;pip-compile&lt;span class="w"&gt; &lt;/span&gt;--generate-hashes&lt;span class="w"&gt; &lt;/span&gt;--extra&lt;span class="o"&gt;=&lt;/span&gt;dev&lt;span class="w"&gt; &lt;/span&gt;--output-file&lt;span class="o"&gt;=&lt;/span&gt;requirements-dev.txt&lt;span class="w"&gt; &lt;/span&gt;pyproject.toml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That ensures whatever you build in production is reproducible in my own machine, to the exact dependency and hash.&lt;/p&gt;
&lt;h2 id="pyprojecttoml-for-configuration"&gt;&lt;a href="https://peps.python.org/pep-0621/"&gt;Pyproject.toml&lt;/a&gt; for configuration&lt;/h2&gt;
&lt;p&gt;Who likes configuration files? I certainly don't. There's nothing nice about having 15 configuration files at the root of my project. One for test coverage, one for linting, one for GitHub, one for formatting, and another one for CI. No thanks.&lt;/p&gt;
&lt;p&gt;Fortunately, &lt;a href="https://peps.python.org/pep-0621/"&gt;PEP 621&lt;/a&gt; happened. And with it, a (mostly) common way to store all of the metadata and configuration for a Python project. Using a &lt;em&gt;single&lt;/em&gt; &lt;code&gt;pyproject.toml&lt;/code&gt;, I can define my local package name and details, my pinned dependencies, my pytest coverage configuration, my formatting configuration, my... You get me. All the configurations, in a &lt;em&gt;single&lt;/em&gt; file. Expand the example below for an example.&lt;/p&gt;
&lt;details&gt;
  &lt;summary&gt;An example &lt;code&gt;pyproject.toml&lt;/code&gt;&lt;/summary&gt;


&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# packaging information&lt;/span&gt;

&lt;span class="k"&gt;[build-system]&lt;/span&gt;
&lt;span class="n"&gt;requires&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;hatchling&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;build-backend&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="s2"&gt;&amp;quot;hatchling.build&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# project information&lt;/span&gt;

&lt;span class="k"&gt;[project]&lt;/span&gt;
&lt;span class="n"&gt;name&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="s2"&gt;&amp;quot;dreambox&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;version&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="s2"&gt;&amp;quot;23.1.26&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;readme&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="s2"&gt;&amp;quot;README.md&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;requires-python&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="s2"&gt;&amp;quot;&amp;gt;=3.10&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# requirements.txt generated from here&lt;/span&gt;

&lt;span class="n"&gt;dependencies&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="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Jinja2&amp;gt;=3.1.2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;loguru&amp;gt;=0.6.0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fastapi&amp;gt;=0.88.0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;uvicorn&amp;gt;=0.20.0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# requirements-dev.txt generated from here&lt;/span&gt;

&lt;span class="k"&gt;[project.optional-dependencies]&lt;/span&gt;
&lt;span class="n"&gt;dev&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="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;black&amp;gt;=22.10.0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;isort&amp;gt;=5.10.1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pip-tools&amp;gt;=6.10.0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pytest&amp;gt;=7.2.0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pytest-cov&amp;gt;=4.0.0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# linting config&lt;/span&gt;

&lt;span class="k"&gt;[tool.ruff]&lt;/span&gt;
&lt;span class="n"&gt;ignore&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;E501&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# isort config&lt;/span&gt;

&lt;span class="k"&gt;[tool.isort]&lt;/span&gt;
&lt;span class="n"&gt;profile&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="s2"&gt;&amp;quot;black&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;line_length&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="mi"&gt;79&lt;/span&gt;
&lt;span class="n"&gt;skip&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.env/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;venv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.venv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# coverage config&lt;/span&gt;

&lt;span class="k"&gt;[tool.coverage.paths]&lt;/span&gt;
&lt;span class="n"&gt;source&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="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;src&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;[tool.coverage.run]&lt;/span&gt;
&lt;span class="n"&gt;branch&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;true&lt;/span&gt;
&lt;span class="n"&gt;relative_files&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;true&lt;/span&gt;

&lt;span class="k"&gt;[tool.coverage.report]&lt;/span&gt;
&lt;span class="n"&gt;show_missing&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;true&lt;/span&gt;
&lt;span class="n"&gt;fail_under&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="mi"&gt;80&lt;/span&gt;

&lt;span class="c1"&gt;# formatting config&lt;/span&gt;

&lt;span class="k"&gt;[tool.black]&lt;/span&gt;
&lt;span class="n"&gt;line-length&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="mi"&gt;79&lt;/span&gt;
&lt;span class="n"&gt;extend-exclude&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="s1"&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;span class="s1"&gt;/(&lt;/span&gt;
&lt;span class="s1"&gt;  | .env&lt;/span&gt;
&lt;span class="s1"&gt;  | .venv&lt;/span&gt;
&lt;span class="s1"&gt;  | venv&lt;/span&gt;
&lt;span class="s1"&gt;  | notebooks&lt;/span&gt;
&lt;span class="s1"&gt;)/&lt;/span&gt;
&lt;span class="s1"&gt;&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;/details&gt;

&lt;h2 id="makefiles-for-common-sense"&gt;&lt;a href="https://makefiletutorial.com/#top"&gt;Makefiles&lt;/a&gt; for common sense&lt;/h2&gt;
&lt;p&gt;I clone a new repo. Now, where the hell do I start? How do I run the tests? How do I run the API? There should be a common standard for these things right? &lt;em&gt;Wrong&lt;/em&gt;. Every project is different. Every project has its pet peeves. I don't really like pet peeves. Anyone should be able to pick up my project and get up and running straight away. No meetings, no calls, just start working. A well-documented &lt;code&gt;README.MD&lt;/code&gt; might work. But you'll get lazy.&lt;/p&gt;
&lt;p&gt;So what is the easiest way to add a good project "map", without much work? Enter my Makefile. With a Makefile, I define all of the main project commands in a single file. For example:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;## Install for production&lt;/span&gt;

&lt;span class="nf"&gt;install&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;--upgrade&lt;span class="w"&gt; &lt;/span&gt;pip
&lt;span class="w"&gt;    &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;.

&lt;span class="c"&gt;## Install for development&lt;/span&gt;

&lt;span class="nf"&gt;install-dev&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;install&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.[dev]&amp;quot;&lt;/span&gt;

&lt;span class="c"&gt;## Build dependencies&lt;/span&gt;

&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;pip-compile&lt;span class="w"&gt; &lt;/span&gt;--resolver&lt;span class="o"&gt;=&lt;/span&gt;backtracking&lt;span class="w"&gt; &lt;/span&gt;--output-file&lt;span class="o"&gt;=&lt;/span&gt;requirements.txt&lt;span class="w"&gt; &lt;/span&gt;pyproject.toml
&lt;span class="w"&gt;    &lt;/span&gt;pip-compile&lt;span class="w"&gt; &lt;/span&gt;--resolver&lt;span class="o"&gt;=&lt;/span&gt;backtracking&lt;span class="w"&gt; &lt;/span&gt;--extra&lt;span class="o"&gt;=&lt;/span&gt;dev&lt;span class="w"&gt; &lt;/span&gt;--output-file&lt;span class="o"&gt;=&lt;/span&gt;requirements-dev.txt&lt;span class="w"&gt; &lt;/span&gt;pyproject.toml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If I want to install the dependencies on a project, all I do is &lt;code&gt;make install&lt;/code&gt;. Or &lt;code&gt;make install-dev&lt;/code&gt; for the dev dependencies as well. This is nice. But my &lt;em&gt;favorite&lt;/em&gt; feature is this:&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/48/make-help.png" alt="Make help command" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;
&lt;p&gt;With the help of &lt;a href="https://gist.github.com/klmr/575726c7e05d8780505a"&gt;this&lt;/a&gt; small script at the end of my Makefile, I can generate a map for my project. Now, whenever someone new comes in, all they have to do is &lt;code&gt;make help&lt;/code&gt;, and they'll get a good idea of how to navigate things. The script takes the comment above every rule in the Makefile, so it's super easy to add new commands to it.&lt;/p&gt;
&lt;p&gt;Now isn't that nice?&lt;/p&gt;
&lt;h2 id="ruff-for-linting"&gt;&lt;a href="https://github.com/charliermarsh/ruff"&gt;Ruff&lt;/a&gt; for linting&lt;/h2&gt;
&lt;p&gt;I like Python, but I also like &lt;a href="/blog/on-rust.html"&gt;Rust&lt;/a&gt; a lot. &lt;a href="https://github.com/charliermarsh/ruff"&gt;Ruff&lt;/a&gt; is a new (and shiny) Python linter written in Rust. It's probably the most modern project in my boilerplate. It does what any linter should do, it should flag errors and bad practices. I like catching errors before they blow up in the user's face.&lt;/p&gt;
&lt;p&gt;Most importantly, Ruff is fast.  &lt;a href="https://www.reddit.com/r/ProgrammingLanguages/comments/v69shk/what_makes_languages_blazingly_fast/"&gt;&lt;em&gt;Blazingly&lt;/em&gt;&lt;/a&gt; fast one might say? Running &lt;code&gt;ruff&lt;/code&gt; in our project will probably take less than half a second. Also, like all other tools I mentioned here, Ruff supports &lt;code&gt;pyproject.toml&lt;/code&gt; for configuration. So you don't have to have to maintain &lt;em&gt;yet&lt;/em&gt; another file. (I'm looking at you &lt;a href="https://github.com/PyCQA/flake8/issues/234"&gt;flake8&lt;/a&gt;)&lt;/p&gt;
&lt;h2 id="black-isort-for-formatting"&gt;&lt;a href="https://black.readthedocs.io/en/stable/"&gt;Black&lt;/a&gt; &amp;amp; &lt;a href="https://pycqa.github.io/isort/"&gt;isort&lt;/a&gt; for formatting&lt;/h2&gt;
&lt;p&gt;I never got the whole tabs vs. spaces thing. But I do know programmers have opinions. So many opinions. A cool thing about working in software is that you can use software to &lt;em&gt;squash&lt;/em&gt; those opinions. We just agree on using a couple of tools to format our code, all other discussions become useless. So thanks, &lt;a href="https://lukasz.langa.pl/"&gt;Łukasz&lt;/a&gt;, for creating &lt;a href="https://github.com/psf/black"&gt;Black&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Most of my projects have a great &lt;code&gt;make format&lt;/code&gt; rule, that runs &lt;code&gt;black&lt;/code&gt; and &lt;code&gt;isort&lt;/code&gt;. The configuration? It's in my &lt;code&gt;pyproject.toml&lt;/code&gt; file above.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;## Format files using black&lt;/span&gt;

&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;isort&lt;span class="w"&gt; &lt;/span&gt;.
&lt;span class="w"&gt;    &lt;/span&gt;black&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Note: Apparently ruff &lt;a href="https://twitter.com/charliermarsh/status/1597661264800813056"&gt;supports&lt;/a&gt; import sorting now? Wow. My boilerplate is already obsolete.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="pre-commit-hooks-i-dont"&gt;pre-commit hooks (I don't)&lt;/h2&gt;
&lt;p&gt;I've seen a lot of projects adopt &lt;a href="https://pre-commit.com/"&gt;pre-commit&lt;/a&gt; hooks as a way of enforcing standards for code. Whether it's to make sure tests pass or to ensure nothing gets raised by linters, pre-commit hooks are all the rage. The thing is, git is already &lt;em&gt;pretty&lt;/em&gt; complicated. I mostly work with data scientists, and most of them already face a steep curve in adopting software practices. From my (opinionated) experience, pre-commit hooks make things even more complicated.&lt;/p&gt;
&lt;p&gt;But yes, some standards should be enforced, otherwise, something will break for sure. But isn't that what CI is for? Enforcing something like formatting and linting is easy with the &lt;code&gt;--check&lt;/code&gt; flag:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c"&gt;## Run checks (ruff + test)&lt;/span&gt;

&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;ruff&lt;span class="w"&gt; &lt;/span&gt;.
&lt;span class="w"&gt;    &lt;/span&gt;isort&lt;span class="w"&gt; &lt;/span&gt;--check&lt;span class="w"&gt; &lt;/span&gt;.
&lt;span class="w"&gt;    &lt;/span&gt;black&lt;span class="w"&gt; &lt;/span&gt;--check&lt;span class="w"&gt; &lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If I run &lt;code&gt;make check&lt;/code&gt; and something doesn't comply, I'll get an error. When opening a PR with a new feature, CI can take care of all of those checks:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;pull_request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v2&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install dev requirements&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;make install-dev&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Check formatting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# &amp;lt;- if this does not pass&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;make check&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Run tests&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# &amp;lt;- you don&amp;#39;t get here&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;make test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If something doesn't run, CI doesn't pass. No need to mess around with git in everyone's machine. Everyone is free to commit whatever they want - and will get notified if something is not working. That's good enough.&lt;/p&gt;
&lt;h2 id="an-example-project"&gt;An example project&lt;/h2&gt;
&lt;p&gt;At some of my previous &lt;a href="/talks"&gt;talks&lt;/a&gt;, people normally ask me to share my project template (e.g., my &lt;a href="https://cookiecutter.readthedocs.io/en/stable/"&gt;cookiecutter&lt;/a&gt;). For me, sharing project templates it's a bit like copying someone else's Vim config. As soon as you need to tweak something (and trust me, you will), you'll have a headache.&lt;/p&gt;
&lt;p&gt;This boilerplate is made of choices (or traumas) I've experienced over time. It's tailored exactly to &lt;em&gt;my&lt;/em&gt; needs (e.g., data science, machine learning, API design). I like &lt;a href="/blog/simple-software.html"&gt;simple&lt;/a&gt; things a lot.&lt;/p&gt;
&lt;p&gt;I do think it could serve some as a starting point for their own &lt;em&gt;opinionated&lt;/em&gt; boilerplate. So &lt;a href="https://github.com/duarteocarmo/boilerplate"&gt;here's&lt;/a&gt; the code, fellow Pythonista. Have fun out there.&lt;/p&gt;
&lt;hr&gt;

&lt;h4 id="updates-notes"&gt;Updates &amp;amp; notes&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;This post was featured in &lt;a href="https://www.youtube.com/live/Mh760W_M2ro?feature=share&amp;amp;t=1040"&gt;Episode #326&lt;/a&gt; of the PythonBytes podcast&lt;/li&gt;
&lt;/ul&gt;</content><category term="blog"/></entry><entry><title>On Rust</title><link href="https://duarteocarmo.com/blog/on-rust.html" rel="alternate"/><published>2023-01-02T00:00:00+01:00</published><updated>2023-01-02T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2023-01-02:/blog/on-rust.html</id><summary type="html">&lt;p&gt;It's that time of the year again. The family is getting together and celebrating. Grandma is cooking something amazing for dinner. I'm trying to solve &lt;a href="https://adventofcode.com/"&gt;Advent of Code&lt;/a&gt; puzzles. This year, I decided to do something &lt;em&gt;different&lt;/em&gt;. Instead of solving the puzzles in Python, I decided that I would try …&lt;/p&gt;</summary><content type="html">&lt;p&gt;It's that time of the year again. The family is getting together and celebrating. Grandma is cooking something amazing for dinner. I'm trying to solve &lt;a href="https://adventofcode.com/"&gt;Advent of Code&lt;/a&gt; puzzles. This year, I decided to do something &lt;em&gt;different&lt;/em&gt;. Instead of solving the puzzles in Python, I decided that I would try to solve them in &lt;a href="https://www.rust-lang.org/"&gt;Rust&lt;/a&gt;. Why?&lt;/p&gt;
&lt;p&gt;Rust has been getting a lot of attention lately. The language is in its &lt;em&gt;7th&lt;/em&gt; year as the &lt;a href="https://survey.stackoverflow.co/2022/#technology-most-loved-dreaded-and-wanted"&gt;most loved programming language&lt;/a&gt; according to the latest Stack Overflow developer survey. Why do people love it so much? So I decided to learn it. What better way to get frustrated during my Christmas break?&lt;/p&gt;
&lt;h2 id="things-i-like"&gt;Things I like&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt; That's the first thing I noticed. I brute force &lt;em&gt;a lot&lt;/em&gt; of these AoC puzzles. Rust handled them without hiccups. It's a bit like the feeling of using Numpy if you're a Python dev. Having been one for some years, I never got much exposure to compiled languages like C++ or Java. I can't help but notice a pretty big difference in speed compared to Python or JavaScript.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Effectiveness.&lt;/strong&gt; Most puzzles I solved were right on the first try. This is rarely the case with Python. Rather than using types as more of a &lt;em&gt;decorative/ergonomic&lt;/em&gt; feature like in Python, in Rust, these types are strictly enforced at compile time. This made me think twice about the code I wrote. But also lead to more correct code. &lt;a href="https://rust-analyzer.github.io/"&gt;Rust-analyzer&lt;/a&gt;, Rust's LSP, was also a great experience. Giving timely, useful, and clear messages about what was wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Syntax.&lt;/strong&gt; The syntax is not that different from Python. For loops are easy and readable with the &lt;code&gt;for x in y&lt;/code&gt; syntax, for example. &lt;a href="https://doc.rust-lang.org/beta/rust-by-example/fn/closures.html"&gt;Closures&lt;/a&gt; remind me a lot of lambda functions in Python. I can also see why Python adopted the &lt;a href="https://doc.rust-lang.org/rust-by-example/flow_control/match.html"&gt;match&lt;/a&gt; statement. In general, Rust was readable. Whenever code got complicated and repetitive, things like &lt;a href="https://doc.rust-lang.org/rust-by-example/macros.html"&gt;macros&lt;/a&gt; came to the rescue.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ecosystem.&lt;/strong&gt; Compared to Pip, &lt;a href="https://doc.rust-lang.org/cargo/guide/index.html"&gt;Cargo&lt;/a&gt; is a breath of fresh air. Installing packages is as easy as copy-pasting into your &lt;code&gt;Cargo.toml&lt;/code&gt; file. In my experience, even larger packages such as &lt;a href="https://docs.rs/nalgebra/latest/nalgebra/"&gt;naglebra&lt;/a&gt; have been fast to install. To check the documentation of all packages installed locally, you can also use &lt;code&gt;cargo doc --open&lt;/code&gt;, which is great for offline development. There are also good resources online when you're feeling stuck, like the &lt;a href="https://doc.rust-lang.org/book/"&gt;Rust Programming Language Book&lt;/a&gt;, or the more practical &lt;a href="https://doc.rust-lang.org/stable/rust-by-example/"&gt;Rust by Example&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="things-i-dont-like-yet"&gt;Things I don't like (yet)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Efficiency.&lt;/strong&gt; Rust takes longer to write. Probably, due to my lack of experience. I get the impression programs need to be better thought out. You'll take longer to write code, but it will be more &lt;em&gt;effective&lt;/em&gt; code. This makes Rust great for production. However, it's hard to think of Rust as a good language to experiment, explore, or even iterate quickly. Experimentation is a great advantage of languages like Python or JavaScript. It's a trade-off.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Types.&lt;/strong&gt; I like types. I've been using types in most of my Python production applications. Not only that, but I believe I now understand better why Python doesn't &lt;em&gt;enforce&lt;/em&gt; types. Types help you write more &lt;em&gt;correct&lt;/em&gt; programs, but it's easy to get stuck in type hell. I don't know if I want a &lt;code&gt;usize&lt;/code&gt; or an &lt;code&gt;int32&lt;/code&gt;. I don't care if it's a &lt;code&gt;Matrix&lt;/code&gt; or a &lt;code&gt;DMatrix&lt;/code&gt;, or &lt;code&gt;f32&lt;/code&gt; or &lt;code&gt;f64&lt;/code&gt;. The Rust compiler does. When I'm focused on solving a problem, I'm not interested in solving a &lt;em&gt;type&lt;/em&gt; problem. Types are great. But if you get too strict about them, types can get in the way. I felt Rust types got in the way a lot of times.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ecosystem.&lt;/strong&gt; Rust's ecosystem is still young. Yes, AoC puzzles are very specific and don't represent the real world. But when looking for libraries, for example, it appears Rust is still trying to figure out where does lie. This is normal. There are not that many resources on the internet &lt;em&gt;yet&lt;/em&gt;. For a beginner programmer in any language, the quality of web resources is everything. When looking for answers and resources on Rust, it takes longer to find the right resources. When looking for libraries, it's tough to understand which libraries are the most popular ones, or the ones to go from. Even though things are developing &lt;a href="https://lib.rs/"&gt;quickly&lt;/a&gt;.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Vector3&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="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;dot_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;float&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="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&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="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&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="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;do_math_by_copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;d2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;f32&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="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&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="n"&gt;p1&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="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&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="n"&gt;p2&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="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;d2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;dot_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&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="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&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="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;do_math_by_borrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;d1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;d2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kp"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;f32&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="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&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="n"&gt;p1&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;d1&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&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="n"&gt;p2&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;d2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&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="n"&gt;dot_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;b&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;b&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Borrowing.&lt;/strong&gt; References and borrowing have been the hardest concepts to grasp for me. Again. This might be very well due to my lack of experience in compiled languages. Look at the example above, taken from &lt;a href="https://www.forrestthewoods.com/blog/should-small-rust-structs-be-passed-by-copy-or-by-borrow/"&gt;this&lt;/a&gt; great post. There is no obvious reason why you would go with &lt;code&gt;do_math_by_borrow&lt;/code&gt;. It's uglier, less readable, and to the best of my knowledge, &lt;em&gt;not&lt;/em&gt; significantly faster. This whole borrowing and ownership dance is probably necessary for Rust. But I felt like doing a lot of &lt;code&gt;.Clone()&lt;/code&gt; and &lt;code&gt;.Copy()&lt;/code&gt; to escape this problem.&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;AoC was great to learn a new language! Perhaps next year something easier though. I didn't finish all the puzzles &lt;a href="https://github.com/duarteocarmo/advent2022/"&gt;by any means&lt;/a&gt;. But by day 10, I had a good grasp of the basics of Rust and could write simple programs without too much hassle. The problem with AoC is the steep curve in puzzle difficulty. By day 15, I was already spending one hour just to parse the input correctly.&lt;/p&gt;
&lt;p&gt;I can understand why Rust is loved. It's fast, effective, and it makes you a better programmer. It requires you to make deliberate choices on every variable you create, and every statement you write. This is a double-edged sword. On one side, it makes you write better and safer code. On the other side, it makes you write code slower, and experiments should be fast. It does look great for production. But it's &lt;a href="https://mdwdotla.medium.com/using-rust-at-a-startup-a-cautionary-tale-42ab823d9454"&gt;early&lt;/a&gt; still.&lt;/p&gt;
&lt;p&gt;I would love to try and write some Machine Learning APIs in Rust. Written in Rust, they might be safer and faster! But for that to happen, I would need to be able to load models using Rust. This means loading Scikit, Transformer, or even PyTorch models with Rust. I don't think we are there yet. But when we are, I would love to give it a go.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Scaling Machine Learning microservices</title><link href="https://semaphore.io/blog/machine-learning-microservice" rel="alternate"/><published>2022-12-18T00:00:00+01:00</published><updated>2022-12-18T00:00:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:semaphore.io,2022-12-18:/blog/machine-learning-microservice</id><content type="html"/><category term="blog"/></entry><entry><title>Monitoring Machine Learning APIs</title><link href="https://duarteocarmo.com/blog/monitoring-machine-learning-apis.html" rel="alternate"/><published>2022-12-06T10:30:00+01:00</published><updated>2022-12-06T10:30:00+01:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2022-12-06:/blog/monitoring-machine-learning-apis.html</id><summary type="html">&lt;p&gt;Last Friday I &lt;a href="/talks"&gt;presented&lt;/a&gt; at PyData Global. I talked about  &lt;em&gt;monitoring&lt;/em&gt;, but felt like there was more I should've said. So this article is just about that.&lt;/p&gt;
&lt;p&gt;Even though the number of models in the wild is growing, the field of monitoring is still green and full of unknowns.&lt;/p&gt;
&lt;p&gt;Almost …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Last Friday I &lt;a href="/talks"&gt;presented&lt;/a&gt; at PyData Global. I talked about  &lt;em&gt;monitoring&lt;/em&gt;, but felt like there was more I should've said. So this article is just about that.&lt;/p&gt;
&lt;p&gt;Even though the number of models in the wild is growing, the field of monitoring is still green and full of unknowns.&lt;/p&gt;
&lt;p&gt;Almost all models I've put into customers' hands have had some sort of monitoring. In my head, I've divided ML monitoring in three main areas.&lt;/p&gt;
&lt;p&gt;Let's explore them.&lt;/p&gt;
&lt;h2 id="logs-error-monitoring"&gt;Logs &amp;amp; Error monitoring&lt;/h2&gt;
&lt;p&gt;This first area is responsible for monitoring three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;Errors&lt;/em&gt;: Something in the application blew up, and needs to be checked (e.g., trace backs, etc.)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Logs&lt;/em&gt;: What's going on? When is it going on?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Performance&lt;/em&gt;: How fast is the API? How much load can it take?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is very similar to traditional software observability. And yes, most machine learning applications - are, in fact, &lt;em&gt;software&lt;/em&gt; applications. This means we have all the concerns of traditional software, and &lt;em&gt;then some&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;When it comes to logging, &lt;a href="https://github.com/Delgan/loguru"&gt;loguru&lt;/a&gt; has made life easier.  At the end of the day, who likes to configure loggers?&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/45/monitoring-sketch-1.png" alt="Logs/Error monitoring of ML Apps" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;Another less known component is &lt;a href="https://opentelemetry.io/"&gt;OpenTelemetry&lt;/a&gt;. OpenTelemetry has slowly become the standard in observability. Think of it as a wrapper that allows you to capture useful statistics on the performance of your app. It also allows you to capture some custom events/data that are useful for debugging.&lt;/p&gt;
&lt;p&gt;Here's an example:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;fastapi&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;opentelemetry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;opentelemetry.exporter.otlp.proto.http.trace_exporter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;OTLPSpanExporter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;opentelemetry.instrumentation.fastapi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPIInstrumentor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;opentelemetry.sdk.trace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TracerProvider&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;opentelemetry.sdk.trace.export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BatchSpanProcessor&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;.models&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;

&lt;span class="c1"&gt;# set up tracing and open telemetry&lt;/span&gt;

&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TracerProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BatchSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OTLPSpanExporter&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_span_processor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_tracer_provider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tracer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# instrument FastAPI&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;demo&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;FastAPIInstrumentor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instrument_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/predict/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reponse_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# get the current span&lt;/span&gt;
    &lt;span class="n"&gt;current_span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_current_span&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# hash input&lt;/span&gt;
    &lt;span class="n"&gt;input_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# save hash to opentelemetry&lt;/span&gt;
    &lt;span class="n"&gt;current_span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;app.demo.input_hash&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;features_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;- Saves attribute&lt;/span&gt;

    &lt;span class="c1"&gt;# return predictions&lt;/span&gt;
    &lt;span class="n"&gt;prediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_prediction_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prediction&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's walk through the example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We kick off manual instrumentation using the &lt;code&gt;TracerProvider&lt;/code&gt;(&lt;a href="https://opentelemetry.io/docs/instrumentation/python/manual/"&gt;docs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;We add automatic instrumentation with the FastAPIInstrumentator (&lt;a href="https://opentelemetry-python-contrib.readthedocs.io/en/latest/instrumentation/fastapi/fastapi.html"&gt;docs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;We have the ability to send custom events with the &lt;code&gt;current_span.set_attribute&lt;/code&gt; function (&lt;a href="https://opentelemetry.io/docs/instrumentation/python/manual/#add-attributes-to-a-span"&gt;docs&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're using something like &lt;a href="https://www.datadoghq.com/"&gt;DataDog&lt;/a&gt;, &lt;a href="https://www.honeycomb.io/"&gt;Honeycomb&lt;/a&gt;, or any other provider that supports OpenTelemetry - you should get all this information on your service's dashboard. You'll get all the stats related to your application, and all the custom events you are sending in as well.&lt;/p&gt;
&lt;p&gt;This is particularly interesting to understand, for example, when your API is breaking, and &lt;em&gt;what&lt;/em&gt; is making it break!&lt;/p&gt;
&lt;h2 id="driftdegradation-monitoring"&gt;Drift/Degradation monitoring&lt;/h2&gt;
&lt;p&gt;The second area of monitoring is about model degradation in the face of new data. This usually happens as models in production start being outdated. Trying to make predictions on types of data your model has never seen before, usually leads to problems. This concept is known as &lt;em&gt;data drift&lt;/em&gt;.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/45/monitoring-sketch-2.png" alt="Drift monitoring" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;In a &lt;a href="/blog/monitoring-ml-models-fastapi-evidently.html"&gt;previous&lt;/a&gt; article, I talked extensively about this topic. I explained how to leverage FastAPI and Evidently to monitor drift, in real-time, for your ml application.&lt;a href="/blog/monitoring-ml-models-fastapi-evidently.html"&gt; So go read that if you're interested&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There is, however, one concept I should double down on. The fact of storing all inputs (e.g., requests) and outputs (e.g., predictions) of your model. Here's an example of doing so - without increasing response latency:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="nd"&gt;@app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/predict/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reponse_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;background_tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BackgroundTasks&lt;/span&gt;&lt;span class="p"&gt;,):&lt;/span&gt;
    &lt;span class="n"&gt;prediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_prediction_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# saves both inputs and outputs as json to our predictions database&lt;/span&gt;
    &lt;span class="c1"&gt;# runs in the background - doesn&amp;#39;t make anyone wait&lt;/span&gt;
    &lt;span class="n"&gt;background_tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;save_to_database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;asdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prediction&lt;/span&gt;

&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This &lt;em&gt;prediction sink&lt;/em&gt; (red in the image above), is key. As described in the previous article, it allows you to monitor drift. But it &lt;em&gt;also&lt;/em&gt; allows you to get insights into the third area of monitoring. Let's talk about that one.&lt;/p&gt;
&lt;h2 id="business-monitoringreporting"&gt;Business monitoring/reporting&lt;/h2&gt;
&lt;p&gt;The third and final area of monitoring relates to &lt;em&gt;everything else&lt;/em&gt; you might want to keep track of. Your team might want to know how many predictions the API has made in the past month, or how many times you've predicted a certain class. Maybe you, or some other team, is tracking KPIs that are highly related to your model. Maybe the finance dept. wants to know the new CTR based on your recommendations.&lt;/p&gt;
&lt;p&gt;The answers to these questions often come in the form of a dashboard.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/45/monitoring-sketch-complete.png" alt="Business monitoring" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;Fortunately, if we store our inputs and predictions in a flexible enough format (e.g., &lt;code&gt;json&lt;/code&gt;), we'll be able to take almost any off-the-shelf tool and make a dashboard. Designing &lt;a href="https://www.metabase.com/"&gt;Metabase&lt;/a&gt; dashboard from your historical &lt;code&gt;json&lt;/code&gt; predictions stored in BigQuery should be straightforward. You could also stick to Google Data Studio. Or use Excel. You get my point, you can use whatever. &lt;code&gt;json&lt;/code&gt; is flexible enough to be sorted by &lt;code&gt;created_at&lt;/code&gt; and pushed into a dashboard.&lt;/p&gt;
&lt;p&gt;Although often &lt;em&gt;overlooked&lt;/em&gt;, this third level of reporting will often debunk the whole "your model is a black box" paradigm. It allows other stakeholders to know exactly what is going on in your model.&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;Yes. Adding all of these different types of monitoring requires &lt;em&gt;some&lt;/em&gt; boilerplate code. And of course, setting this up might take some time. &lt;em&gt;Especially&lt;/em&gt; if you do these to every model in production.&lt;/p&gt;
&lt;p&gt;However, it's certainly impressive just how much insight you can get from some OpenTelemetry auto-instrumentation and a background job to save all your predictions.&lt;/p&gt;
&lt;p&gt;Sufficiently monitoring your ML model increases your team's confidence when putting models out there. It's a bit like testing - you are now more sure things are happening as they should. With the third area of monitoring, you can then extend that feeling to the rest of the organization.&lt;/p&gt;
&lt;p&gt;"Machine learning is such a black box". Open some doors to the box, and let other people in.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Goodbye Apple Watch</title><link href="https://duarteocarmo.com/blog/coros-pace-2-apple-watch.html" rel="alternate"/><published>2022-10-29T12:00:00+02:00</published><updated>2022-10-29T12:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2022-10-29:/blog/coros-pace-2-apple-watch.html</id><summary type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/44/cover_small_optim.png" alt="Coros pace 2 outside shot" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;1768 days.&lt;/p&gt;
&lt;p&gt;That's how long I've used the Apple Watch. Just a little under 5 years, in case you're wondering. I really love it. I don't know about "future of computing" and all that, but it's definitely a device I've fallen in love with. Last week, for the first time …&lt;/p&gt;</summary><content type="html">&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/44/cover_small_optim.png" alt="Coros pace 2 outside shot" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;1768 days.&lt;/p&gt;
&lt;p&gt;That's how long I've used the Apple Watch. Just a little under 5 years, in case you're wondering. I really love it. I don't know about "future of computing" and all that, but it's definitely a device I've fallen in love with. Last week, for the first time, I ditched it. Let me explain why.&lt;/p&gt;
&lt;p&gt;My last marathon didn't go that well. A story for another day. In the aftermath, I've been thinking about my training process. What went well, what went wrong, you know, introspection and all that. So I did what I do best. I got too deep into the topic. I research how other people train. I explore the vast sea of resources about running. You come across some &lt;a href="https://fellrnr.com/wiki/Main_Page"&gt;pretty cool&lt;/a&gt; resources. And some &lt;a href="http://www.electricblues.com/html/runpro.html"&gt;scary&lt;/a&gt; ones.&lt;/p&gt;
&lt;p&gt;I was watching Kipchoge, and how he just &lt;a href="https://worldathletics.org/news/report/eliud-kipchoge-world-record-berlin-marathon-2022"&gt;broke the WR in Berlin&lt;/a&gt;. What a guy. You know what watch he uses for running? It's called &lt;a href="https://www.coros.com/pace2"&gt;Coros Pace 2&lt;/a&gt;. I'm far from a Kipchoge, but for 200 EUR, I had to know more. After some weeks of &lt;em&gt;e-window&lt;/em&gt; shopping and a lot of YouTube reviews, I ordered it.&lt;/p&gt;
&lt;h2 id="the-good"&gt;The good&lt;/h2&gt;
&lt;p&gt;20 days. That's how much this watch can last in a single charge. Yes, you read it right. &lt;em&gt;20 days&lt;/em&gt;. For someone who is used to charging an Apple Watch every single day, that's a pretty nice upgrade. I don't have to worry about charging it every night, I don't have to carry an extra cable every single time I travel. I just go, and it will last the whole trip.&lt;/p&gt;
&lt;p&gt;Obviously, I bought this watch for the fitness capabilities. Compared to the Apple Watch, it delivers. Setting up the Apple Watch for interval training, or any type of specific workout can be a pain. It doesn't even come &lt;em&gt;close&lt;/em&gt; to the Coros. The workout views are much more informative and the after-workout analyses are much more insightful. That'
s expected. What I didn't expect, is a full web-based training portal where I can track all my workout metrics. Now that's a nice surprise.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/44/traininghub.png" alt="Coros training hub" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;p&gt;The training hub and the Coros app (which is not &lt;em&gt;that&lt;/em&gt; bad) give me much better control of my training plan. I can plan 3 months in advance and design all my workouts exactly like I want. For every workout, Coros gives me the Aerobic and Anaerobic training effect, my effort, my performance, so I know exactly what went down.&lt;/p&gt;
&lt;p&gt;Sleep tracking. You know that thing you &lt;em&gt;sometimes&lt;/em&gt; do with your Apple Watch, but not &lt;em&gt;too&lt;/em&gt; often since you don't know if you'll have time to charge in the morning? Yup. Since I don't have to charge the Coros overnight, I get access to all my Sleep stats. I know exactly how much and how well I've slept over time. You know, 'cause sleep &lt;a href="https://www.nature.com/articles/4371207a"&gt;matters&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-bad"&gt;The bad&lt;/h2&gt;
&lt;p&gt;It's not an Apple Watch. Yes, I could write about the fact that some parts of the Coros app are kinda rough, or the fact that the watch interface is a bit clunky. Or even the fact that it uses some sort of weird proprietary charger. But the bottom line is, I keep comparing it to the Apple Watch.&lt;/p&gt;
&lt;p&gt;One side of me now understands why Apple has never given us custom watch faces. Coros has &lt;em&gt;at least&lt;/em&gt; 100 watch faces to chose from, and they all look… well,  not great. This watch is clearly missing the aesthetic appeal that the Apple Watch has.&lt;/p&gt;
&lt;p&gt;I have a love/hate relationship with Apple's beautiful walled garden of an ecosystem. How can I miss paying with my wrist so much? How can I miss controlling music playback on my wrist so much? Seeing those circles close in every widget I own. Getting all the notifications in your watch. The next event in your calendar at wrist glance. The "Do not disturb until I leave this location", two taps away. Even the haptic motor.&lt;/p&gt;
&lt;p&gt;I miss the cozy ecosystem. It's a bit like a withdrawal - it will fade. I think.&lt;/p&gt;
&lt;center&gt;
&lt;img src="https://duarteocarmo.com/images/44/home.png" alt="Coros pace 2 outside shot at home" style="max-width:100%;border-radius: 2px"&gt;
&lt;/center&gt;

&lt;h2 id="the-end"&gt;The end&lt;/h2&gt;
&lt;p&gt;I don't think there's a winner. They're &lt;em&gt;different&lt;/em&gt; devices - with different goals, and different target audiences. One, focuses on design, ecosystem, wellness, and lifestyle. The other is a powerful fitness tracker, and does it very well.&lt;/p&gt;
&lt;p&gt;I &lt;em&gt;could&lt;/em&gt; just use both. But I don't want to add that stress to my life. For now, a good run brings me more joy than paying with my wrist. A good night of sleep is more important than seeing all my calendar events in my wrist. &lt;em&gt;Focus&lt;/em&gt;, is more important than constant notifications.&lt;/p&gt;
&lt;p&gt;So I'll stick to the Coros for now.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>infrequent.app - stay in touch with those who matter</title><link href="https://duarteocarmo.com/blog/infrequent.html" rel="alternate"/><published>2022-10-23T19:00:00+02:00</published><updated>2022-10-23T19:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2022-10-23:/blog/infrequent.html</id><summary type="html">&lt;p&gt;Some months ago, I &lt;a href="infrequent-tiny-crm.html"&gt;built&lt;/a&gt; a small script to help me stay in touch with those who matter to me. The concept is fairly simple: I want to stay in touch with person &lt;em&gt;p&lt;/em&gt; every &lt;em&gt;t&lt;/em&gt; period of time. If &lt;em&gt;t&lt;/em&gt; has passed, and you haven't talked to &lt;em&gt;p&lt;/em&gt;, you …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Some months ago, I &lt;a href="infrequent-tiny-crm.html"&gt;built&lt;/a&gt; a small script to help me stay in touch with those who matter to me. The concept is fairly simple: I want to stay in touch with person &lt;em&gt;p&lt;/em&gt; every &lt;em&gt;t&lt;/em&gt; period of time. If &lt;em&gt;t&lt;/em&gt; has passed, and you haven't talked to &lt;em&gt;p&lt;/em&gt;, you get a reminder.&lt;/p&gt;
&lt;p&gt;I've been using this script/system for the past 5 months, it has served me well. I've been keeping up with mentors, ex-managers, family, and friends - without much effort. Which is the goal. It's not that I don't like editing a bunch of markdown files every week - but I wanted something &lt;em&gt;smoother&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Also, the more I've told people about my system (which is &lt;a href="https://sive.rs/hundreds"&gt;not even mine&lt;/a&gt;), the more I've realized that I'm not the only one who needs something like this. Generally, people have a &lt;em&gt;hard&lt;/em&gt; time keeping in touch. It's not that we don't care - it's that life gets in the way.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://infrequent.app"&gt;
&lt;img src="https://duarteocarmo.com/images/43/website.png" alt="Infrequent dashboard" style="max-width:100%;"&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And so I built &lt;a href="https://infrequent.app"&gt;infrequent&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Infrequent is a system to stay in touch with those I care about. A system that I built &lt;em&gt;literally&lt;/em&gt; for myself, to scratch my own itch. But also a system that I hope helps others out there.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://infrequent.app"&gt;
&lt;img src="https://duarteocarmo.com/images/43/dashboard.png" alt="Infrequent dashboard" style="max-width:100%;"&gt;
&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I'm not gonna lie, it's been pretty fun to get back into web development. I love AI&amp;amp;ML, but there's something &lt;em&gt;special&lt;/em&gt; about web development. And &lt;a href="https://www.djangoproject.com/"&gt;Django&lt;/a&gt;, &lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt;, and &lt;a href="https://tailwindcss.com/"&gt;tailwind&lt;/a&gt; have been a joy to work with. The stack is not straightforward to set up, but once you get going, it's a breeze to work with. Django is an &lt;em&gt;incredibly&lt;/em&gt; powerful framework.&lt;/p&gt;
&lt;p&gt;I don't have million-dollar plans for infrequent just yet. For now, I plan to continue using it, and entice others to do so too. I'll keep adding features to it, as mine (and maybe your) needs evolve. Feel free to give it a spin, and don't hesitate to &lt;a href="mailto:me@duarteocarmo.com"&gt;email me&lt;/a&gt; if you like it, or think something is missing.&lt;/p&gt;</content><category term="blog"/></entry><entry><title>Monitoring ML models with FastAPI and Evidently AI</title><link href="https://duarteocarmo.com/blog/monitoring-ml-models-fastapi-evidently.html" rel="alternate"/><published>2022-09-17T15:00:00+02:00</published><updated>2022-09-17T15:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2022-09-17:/blog/monitoring-ml-models-fastapi-evidently.html</id><summary type="html">&lt;p&gt;I've deployed a good amount of ML models to production. For many, deployment to production is the last step of the process. Once that's done, work is done. This is far from true. Once your model is out there, problems &lt;em&gt;will&lt;/em&gt; start to arise. Some predictions will be wrong. Some …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've deployed a good amount of ML models to production. For many, deployment to production is the last step of the process. Once that's done, work is done. This is far from true. Once your model is out there, problems &lt;em&gt;will&lt;/em&gt; start to arise. Some predictions will be wrong. Some labels will occur more often than they should, and some examples &lt;em&gt;will&lt;/em&gt; surprise the model.&lt;/p&gt;
&lt;p&gt;Setting up the right lenses &amp;amp; triggers &lt;em&gt;into&lt;/em&gt; your model is critical. It helps to ensure everything is running smoothly, or to know when issues need to be tackled. Let's open up the black box.&lt;/p&gt;
&lt;h2 id="setting-up-fastapi"&gt;Setting up FastAPI&lt;/h2&gt;
&lt;p&gt;I've previously &lt;a href="/blog/serving-ml-models-fastapi.html"&gt;written&lt;/a&gt; about how to serve your ML model with FastAPI. Let's assume we're serving our model with FastAPI and our &lt;code&gt;src&lt;/code&gt; folder looks something like:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;src
├──&lt;span class="w"&gt; &lt;/span&gt;__init__.py
├──&lt;span class="w"&gt; &lt;/span&gt;api&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;-&lt;span class="w"&gt; &lt;/span&gt;your&lt;span class="w"&gt; &lt;/span&gt;Fast&lt;span class="w"&gt; &lt;/span&gt;API&lt;span class="w"&gt; &lt;/span&gt;folder
│&lt;span class="w"&gt;   &lt;/span&gt;├──&lt;span class="w"&gt; &lt;/span&gt;__init__.py
│&lt;span class="w"&gt;   &lt;/span&gt;├──&lt;span class="w"&gt; &lt;/span&gt;models.py
│&lt;span class="w"&gt;   &lt;/span&gt;└──&lt;span class="w"&gt; &lt;/span&gt;main.py
├──&lt;span class="w"&gt; &lt;/span&gt;pipeline
│&lt;span class="w"&gt;   &lt;/span&gt;├──&lt;span class="w"&gt; &lt;/span&gt;__init__.py
│&lt;span class="w"&gt;   &lt;/span&gt;├──&lt;span class="w"&gt; &lt;/span&gt;...
│&lt;span class="w"&gt;   &lt;/span&gt;└──&lt;span class="w"&gt; &lt;/span&gt;train.py
└──&lt;span class="w"&gt; &lt;/span&gt;setup.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;main.py&lt;/code&gt; file is where our views are defined. Below, our main file, with a &lt;code&gt;/predict&lt;/code&gt; endpoint, that serves predictions.&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.py&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;fastapi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;src.pipeline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_prediction_for&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;.models&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;joblib&lt;/span&gt;

&lt;span class="c1"&gt;# create FastAPI app and load model&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;joblib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;model.joblib&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# create an endpoint that receives POST requests&lt;/span&gt;

&lt;span class="nd"&gt;@app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/predict/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reponse_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# some processing&lt;/span&gt;
    &lt;span class="n"&gt;prediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_prediction_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prediction&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="prerequisite-storing-all-predictions"&gt;Prerequisite: Storing all predictions&lt;/h2&gt;
&lt;p&gt;To monitor our model, we must first make sure two things are happening:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prediction logging:  We're actively logging all predictions our model is making&lt;/li&gt;
&lt;li&gt;Access to a reference dataset: We have access to the dataset where our model was trained  (e.g., the training data)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Logging all predictions your model makes can be done using some managed database service (e.g., think Aurora, BigQuery, etc.).  Ideally, we want to do this without increasing our prediction latency.&lt;/p&gt;
&lt;p&gt;Fortunately, Fast API provides gives a great tool to do this: &lt;code&gt;BackgroundTasks&lt;/code&gt;. We start by creating a function that saves our data (in this example, to BigQuery):&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# monitoring.py&lt;/span&gt;

&lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;save_to_database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="sd"&gt;    Saves input/output dicts to bigquery&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BigQuery&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;your_cool_bq_table&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;current_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;rows_to_insert&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;current_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())]&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert_rows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;rows_to_insert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Saved prediction&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can now add it to our API as a background task:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.py&lt;/span&gt;

&lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;fastapi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BackgroundTasks&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;.monitoring&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;save_to_database&lt;/span&gt;

&lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="c1"&gt;# create an endpoint that receives POST requests&lt;/span&gt;

&lt;span class="nd"&gt;@app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/predict/&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;reponse_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;background_tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundTasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# some processing&lt;/span&gt;
    &lt;span class="n"&gt;prediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_prediction_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;background_tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;save_to_bq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prediction&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice how the background task does not block the prediction time. Allowing us to keep prediction latency as low as possible, while &lt;em&gt;still&lt;/em&gt;, saving all predictions.&lt;/p&gt;
&lt;h2 id="setting-up-the-monitoring"&gt;Setting up the monitoring&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://docs.evidentlyai.com/"&gt;Evidently&lt;/a&gt; is a great open-source tool that allows you to set up monitoring for your ML models. It's not the only one, there's a myriad of them, actually. &lt;a href="https://www.nannyml.com/"&gt;nannyML&lt;/a&gt; is another one.&lt;/p&gt;
&lt;p&gt;Evidently allows you to generate a &lt;a href="https://docs.evidentlyai.com/features/dashboards/input_data#dataset-structure"&gt;bunch of different reports&lt;/a&gt; you can generate. In this example, I'll focus on the Data Drift dashboard.&lt;/p&gt;
&lt;p&gt;The Data Drift dashboard allows you to measure the difference in distribution between the predictions you are making, and the labels of your training set. When these two start to become &lt;em&gt;significantly&lt;/em&gt; different, you are likely encountering some drift.&lt;/p&gt;
&lt;p&gt;Alright, let's build it. We start by creating a couple of functions in our &lt;code&gt;monitoring.py&lt;/code&gt; module:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# ... rest of the monitoring.py&lt;/span&gt;

&lt;span class="n"&gt;DATA_WINDOW_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="c1"&gt;# how many predictions to load&lt;/span&gt;

&lt;span class="c1"&gt;# loads our training/reference dataset&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;load_train_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;train_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;static/train_data.csv&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;train_df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;train_df&lt;/span&gt;

&lt;span class="c1"&gt;# loads our latest predictions&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;load_last_predictions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;    SELECT created_at, input, output&lt;/span&gt;
&lt;span class="s2"&gt;    FROM `my_cool_bgq_table`&lt;/span&gt;
&lt;span class="s2"&gt;    ORDER BY created_at DESC&lt;/span&gt;
&lt;span class="s2"&gt;    LIMIT &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DATA_WINDOW_SIZE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;;&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;prediction_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_gbq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prediction_data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now that we're able to fetch both our reference data and our past predictions, we're ready to build our Data Drift dashboard:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# ... rest of the monitoring.py&lt;/span&gt;

&lt;span class="c1"&gt;# this function generates a dashboard from our reference and prediction data&lt;/span&gt;

&lt;span class="c1"&gt;# which is then saved to a `drift.html` file&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;generate_dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;dasboard_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;static/drift.html&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;data_drift_dashboard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Dashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;tabs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;DataDriftTab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verbose_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;reference_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load_reference_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;current_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load_last_predictions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;data_drift_dashboard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;reference_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reference_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;current_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;current_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;column_mapping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;data_drift_dashboard&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dasboard_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Dashboard saved to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dasboard_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dasboard_name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice how we're creating our dashboard, and then saving it to a &lt;code&gt;static/drift.html&lt;/code&gt; file. The idea is then to serve this dashboard in one of our FastAPI endpoints.&lt;/p&gt;
&lt;h2 id="monitoring-dashboard"&gt;Monitoring dashboard&lt;/h2&gt;
&lt;p&gt;Let's serve our data drift dashboard:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;.monitoring&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;generate_dashboard&lt;/span&gt;

&lt;span class="c1"&gt;# ... rest of the main.py&lt;/span&gt;

&lt;span class="nd"&gt;@app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/monitoring&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Other&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;monitoring&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;dashboard_location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate_dashboard&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;FileResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dashboard_location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Every time we visit &lt;code&gt;/monitoring&lt;/code&gt;, Fast API will run the &lt;code&gt;generate_dashboard&lt;/code&gt; function, and return an &lt;code&gt;html&lt;/code&gt; file:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/42/dashboard.png" alt="Monitoring Dashboard" style="max-width:100%;"&gt;&lt;/p&gt;
&lt;p&gt;As you can see, this dashboard compares the distribution of our reference and &lt;em&gt;current&lt;/em&gt; dataset. The current dataset being the latest Y predictions we've made.&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;I've found this to be a &lt;em&gt;relatively&lt;/em&gt;  straightforward way of adding a bit of visibility to what's really happening in my production models. If those distributions are looking particularly skewed: you know it's time to act.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://evidentlyai.com/"&gt;Evidently&lt;/a&gt; allows us to generate much more than just a data drift dashboard. You can also generate dashboards to monitor data quality, the performance of a regression, classification performance, and many more. It's worth taking a look at their docs to see what fits your use case best.&lt;/p&gt;
&lt;p&gt;There's a way we could increase the speed here. Instead of computing the entire dashboard every time we visit &lt;code&gt;/monitoring&lt;/code&gt;, we &lt;a href="https://fastapi-utils.davidmontague.xyz/user-guide/repeated-tasks/#the-repeat_every-decorator"&gt;could&lt;/a&gt; compute it every X time period in the &lt;em&gt;background&lt;/em&gt;. This would result in much faster response from the &lt;code&gt;/monitoring&lt;/code&gt; endpoint.&lt;/p&gt;
&lt;p&gt;Is this dashboard enough to make sure everything is going well in production? No. But it's a &lt;em&gt;great&lt;/em&gt; first step towards figuring out what's &lt;em&gt;really&lt;/em&gt; going on.&lt;/p&gt;
&lt;hr&gt;

&lt;h4 id="updates-notes"&gt;Updates &amp;amp; notes&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://mlops.community/monitoring-ml-models-with-fastapi-and-evidently-ai/"&gt;This post was republished by the MLOps Community blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;I also wrote a &lt;a href="/blog/monitoring-machine-learning-apis.html"&gt;more general post&lt;/a&gt; on how to monitor machine learning APIs&lt;/li&gt;
&lt;/ul&gt;</content><category term="blog"/></entry><entry><title>Down from the Cloud</title><link href="https://duarteocarmo.com/blog/down-from-the-cloud-self-hosting.html" rel="alternate"/><published>2022-08-05T11:00:00+02:00</published><updated>2022-08-05T11:00:00+02:00</updated><author><name>Duarte O.Carmo</name></author><id>tag:duarteocarmo.com,2022-08-05:/blog/down-from-the-cloud-self-hosting.html</id><summary type="html">&lt;p&gt;I love the Cloud. For years, I've been deploying software to it. Azure, GCP, AWS, you name it. I've used most of them. To be honest, they're the same pig, but with different lipstick (like the Danes say). Package up your app, make some CI/CD magic, select your service …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I love the Cloud. For years, I've been deploying software to it. Azure, GCP, AWS, you name it. I've used most of them. To be honest, they're the same pig, but with different lipstick (like the Danes say). Package up your app, make some CI/CD magic, select your service, and I'm good to go. Still remember when I helped a whole bank move to the Public Cloud. The benefits are mostly obvious.&lt;/p&gt;
&lt;p&gt;But I'm no bank. And it's not all rainbows and unicorns. For starters, those 10 elastic beanstalk applications can start adding up. What if I want to change from Cloud A to Cloud B? It's not like Amazon is going to make my life easy. It's a convenience/lock-in trade-off. What about user data? What about &lt;em&gt;my&lt;/em&gt; data? Some of the &lt;a href="https://marco.org/2014/02/23/the-value-of-background-fetch"&gt;best&lt;/a&gt; &lt;a href="https://twitter.com/levelsio/status/1308145873843560449"&gt;software&lt;/a&gt; I use doesn't even use the Cloud.&lt;/p&gt;
&lt;p&gt;Let's come down from the Cloud (at least the public one): Self-hosting. How &lt;em&gt;hard&lt;/em&gt; is it, really?&lt;/p&gt;
&lt;h2 id="get-a-server"&gt;Get a server&lt;/h2&gt;
&lt;p&gt;This one should be straightforward. There's no shortage of server providers. Just browsed &lt;a href="https://lowendbox.com/"&gt;LowEndBox&lt;/a&gt; for a while I decided to go with a dedicated server from &lt;a href="https://www.hetzner.com/"&gt;Hetzner&lt;/a&gt;. Something relatively close to Copenhagen to ensure my connection to the server is snappy enough.&lt;/p&gt;
&lt;p&gt;I don't run a lot of very high traffic sites (yet!). Something around the 50 EUR/month price point serves just fine. You'll be surprised with how much stuff you can run in this machine. I have 10 services running and my memory barely goes above 10%. Let's not jinx it.&lt;/p&gt;
&lt;h2 id="containerize-all-the-things"&gt;Containerize all the things&lt;/h2&gt;
&lt;p&gt;I have a love-hate relationship with Docker. For me, it's still the most straightforward way of exchanging and packaging up software to ensure nothing breaks. But how I hate when my container starts getting fat. I'm talking about you, PyTorch.&lt;/p&gt;
&lt;p&gt;Every application will run inside its own containerized environment. To ensure this doesn't get overly complicated, I went with &lt;code&gt;docker compose&lt;/code&gt;. Why not Kubernetes you may ask? Because I have other things to do.&lt;/p&gt;
&lt;p&gt;Inside the server, things look a little something like:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;|&lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;Caddyfile&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;-&lt;span class="w"&gt; &lt;/span&gt;more&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;this&lt;span class="w"&gt; &lt;/span&gt;later
&lt;span class="sb"&gt;`&lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;projects
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;project-1&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;-&lt;span class="w"&gt; &lt;/span&gt;first&lt;span class="w"&gt; &lt;/span&gt;application
&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="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="p"&gt;|&lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;Dockerfile
&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="sb"&gt;`&lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;docker-compose.yml&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;-&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;file
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;project-2&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;-&lt;span class="w"&gt; &lt;/span&gt;second&lt;span class="w"&gt; &lt;/span&gt;application
&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;Dockerfile
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;docker-compose.yml&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;-&lt;span class="w"&gt; &lt;/span&gt;second&lt;span class="w"&gt; &lt;/span&gt;docker&lt;span class="w"&gt; &lt;/span&gt;compose&lt;span class="w"&gt; &lt;/span&gt;file
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Every project gets its own &lt;code&gt;docker-compose.yml&lt;/code&gt; file. Every project runs on its specific server port. For example, to run &lt;code&gt;project-1&lt;/code&gt;, I &lt;code&gt;cd&lt;/code&gt; into the directory and start the container: &lt;code&gt;docker compose up --force-recreate --build -d&lt;/code&gt;. This automatically starts the service on the port I specify on the &lt;code&gt;docker-compose.yml&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;Using the process above, I can have a bunch of different applications running on a lot of different ports, but in a single server. Hopefully (1) saving money, and (2) increasing the power and control over them.&lt;/p&gt;
&lt;p&gt;Hopefully, I won't land on dependency-nightmare-land.&lt;/p&gt;
&lt;h2 id="continuously-deploy"&gt;Continuously deploy&lt;/h2&gt;
&lt;p&gt;Self-hosting sounds great. Having to &lt;code&gt;ssh&lt;/code&gt; into a server &lt;em&gt;every time&lt;/em&gt; I want to update a service, doesn't. If there's one thing I'm not willing to compromise on, it's continuous deployments. The whole &lt;code&gt;git push&lt;/code&gt; automatically updates the app thing is super convenient.&lt;/p&gt;
&lt;p&gt;Enter GitHub actions. With them, you can automate the ssh'ing and re-deployment part. It's not the most &lt;em&gt;secure&lt;/em&gt; option. But hey, it works pretty well. Surprisingly, it's also much faster than deploying on Elastic Beanstalk or Cloud Run.&lt;/p&gt;
&lt;p&gt;Here's what these actions look like:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Deploy to server&lt;/span&gt;
&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;master&lt;/span&gt;
&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Checkout source code&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v1&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@master&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Copies repository to server&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;appleboy/scp-action@master&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.HOST }} &amp;lt;- these are defined in your repo settings.&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.USERNAME }}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.PASSWORD }}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;22&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;overwrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;.&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/root/projects/project-X&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;appleboy/ssh-action@master&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Stops and updates docker container as deamon&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.HOST }}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.USERNAME }}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.PASSWORD }}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;22&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;cd projects/project-X&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;docker compose down&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;docker compose up --force-recreate --build -d&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;docker ps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="https-we-meet-again"&gt;HTTPS, we meet again&lt;/h2&gt;
&lt;p&gt;Alright, it's time to expose these apps to the world. I'll admit it. The sheer mentioning of &lt;em&gt;https certificates&lt;/em&gt; or &lt;em&gt;DNS&lt;/em&gt; runs a chill down my spine. After loosing a couple of hours in the &lt;a href="https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/"&gt;reverse proxy documentation for Nginx&lt;/a&gt; and getting a bit mad, I found this little thing called &lt;a href="https://caddyserver.com/"&gt;Caddy&lt;/a&gt;. Oh boy, that was easy.&lt;/p&gt;
&lt;p&gt;Caddy solves the whole subdomain, reverse proxy, multiple websites on the same server, HTTPS &lt;em&gt;mambo jumbo&lt;/em&gt;. The docs are great, and it's super straightforward to use. I definitely recommend their &lt;a href="https://caddyserver.com/docs/getting-started"&gt;Getting Started&lt;/a&gt; docs.&lt;/p&gt;
&lt;p&gt;First step is to point some A record in your DNS to your server's IP address. I use Cloudflare for this, do recommend. Once Caddy is installed in the server, all you need to do is create a &lt;code&gt;Caddyfile&lt;/code&gt;:&lt;/p&gt;
&lt;div class="codehilite"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;project-1.mydomain.com {
        reverse_proxy localhost:3000
}

project-2.mydomain.com {
        reverse_proxy localhost:5000
}

home.mydomain.com {
        respond &amp;quot;Base domain&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After that, run &lt;code&gt;caddy start&lt;/code&gt;, and we're done. Yes. Your eyes are not kidding you. That's absolutely it. &lt;code&gt;project-1.mydomain.com&lt;/code&gt; is connected to my app running on port &lt;code&gt;3000&lt;/code&gt;, and &lt;code&gt;project-2.mydomain.com&lt;/code&gt; is connected to the container on port &lt;code&gt;5000&lt;/code&gt;. HTTPS? Automatic. Reverse proxy? Done.&lt;/p&gt;
&lt;p&gt;Caddy automatically enables HTTPS for all of the apps you specify on your &lt;code&gt;Caddyfile&lt;/code&gt;. It also automatically issues new certificates and renews them. Changes? A simple &lt;code&gt;caddy reload&lt;/code&gt; and you're up and running again.&lt;/p&gt;
&lt;p&gt;I can have as many subdomains/domains pointing to the same server as I want, as long as the server can handle it.&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;Closing thoughts&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://duarteocarmo.com/images/41/glances.png" alt="Glances" style="max-width:100%;"&gt;&lt;/p&gt;
&lt;p&gt;One of the services I'm currently self hosting is called &lt;a href="https://nicolargo.github.io/glances/"&gt;Glances&lt;/a&gt;. With it, all of my server's metrics an url away. With about 5 containers, the CPU is still below 10%.&lt;/p&gt;
&lt;p&gt;No. I don't have all the elasticity that the Public Cloud gives me. But do I need it? The deployment method is the same (&lt;code&gt;git push&lt;/code&gt;), the speed of services are the same (mostly static with Cloudflare on top), the cost is lower, and data is controlled by me. Sounds like a treat.&lt;/p&gt;</content><category term="blog"/></entry></feed>