<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://theophilec.github.io/feed.xml" rel="self" type="application/atom+xml"/><link href="https://theophilec.github.io/" rel="alternate" type="text/html" hreflang="en"/><updated>2026-09-30T19:44:06+00:00</updated><id>https://theophilec.github.io/feed.xml</id><title type="html">blank</title><subtitle>ML Engineer and PhD </subtitle><entry><title type="html">datasette-keyboard-shortcuts: adding keyboard shortcuts to datasette</title><link href="https://theophilec.github.io/blog/2026/datasette-keyboard-shortcuts/" rel="alternate" type="text/html" title="datasette-keyboard-shortcuts: adding keyboard shortcuts to datasette"/><published>2026-08-23T00:00:00+00:00</published><updated>2026-08-23T00:00:00+00:00</updated><id>https://theophilec.github.io/blog/2026/datasette-keyboard-shortcuts</id><content type="html" xml:base="https://theophilec.github.io/blog/2026/datasette-keyboard-shortcuts/"><![CDATA[<p><code class="language-plaintext highlighter-rouge">datasette-keyboard-shortcuts</code> is a Datasette keyboard shortcut plugin I published on <a href="https://github.com/theophilec/datasette-keyboard-shortcuts/">Github</a> and on <a href="https://pypi.org/project/datasette-keyboard-shortcuts/">PyPi</a>.</p> <p>Installing it in you datasette virtual environment allows you to add and configure datasette keyboard shortcuts, through other plugins.</p> <p>Initially, I wanted to add shortcuts for running and formatting queries. Figured, I’d make it generic so others could add their own use cases.</p> <p>Currently running and formatting queries are built-in actions. The plugin exposes a hook to allow other plugins to register additional plugins. To illustrate this, I had Claude write a <a href="https://github.com/theophilec/datasette-homepage-shortcut">plugin</a> to map <code class="language-plaintext highlighter-rouge">CMD + SHIFT + H</code> to going to the Datasette homepage.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[datasette-keyboard-shortcuts is a Datasette keyboard shortcut plugin I published on Github and on PyPi.]]></summary></entry><entry><title type="html">datasette-syntaqlite: adding SQL linting to Datasette</title><link href="https://theophilec.github.io/blog/2026/datasette-syntaqlite/" rel="alternate" type="text/html" title="datasette-syntaqlite: adding SQL linting to Datasette"/><published>2026-04-06T00:00:00+00:00</published><updated>2026-04-06T00:00:00+00:00</updated><id>https://theophilec.github.io/blog/2026/datasette-syntaqlite</id><content type="html" xml:base="https://theophilec.github.io/blog/2026/datasette-syntaqlite/"><![CDATA[<p><code class="language-plaintext highlighter-rouge">datasette-syntaqlite</code> is a <a href="https://datasette.io/">Datasette</a> plugin that augments Datasette’s query editor using <a href="https://github.com/LalitMaganti/syntaqlite"><code class="language-plaintext highlighter-rouge">syntaqlite</code></a>. Currently it catches syntax errors and incorrect columns based on available tables and views.</p> <p><img src="/assets/img/datasette-syntaqlite.png" alt="datasette-syntaqlite"/></p> <p>You can check out the code <a href="https://github.com/theophilec/datasette-syntaqlite">here</a> or add it to your Datasette project using <code class="language-plaintext highlighter-rouge">uv add datasette-syntaqlite</code>.</p> <p>I use Datasette on a weekly basis for a lot of my side projects and while data wrangling for my <a href="https://mines-paris.org">volunteering</a>. I have been wanting to improve the experience for my use cases and <a href="https://github.com/LalitMaganti/syntaqlite">syntaqlite</a>’s creator <a href="https://lalitm.com">Lalit Maganti</a>’s <a href="https://lalitm.com/post/building-syntaqlite-ai/">blog post</a> spurred me to spend a few hours building this tool. Claude (in Zed) wrote a first version I then rewrote. The js part is largely untouched as I am not familiar with how to make it better.</p> <p>This is a side project but I am interested in pushing it further:</p> <ul> <li>Integrating the language server aspect with edit prediction</li> <li>Replacing Datasette’s formatting with syntaqlite’s</li> </ul> <p>Oh and this is unrelated but I’d like to add a <code class="language-plaintext highlighter-rouge">Cmd+Enter</code> run query keyboard shortcut like in <a href="https://metabase.com">Metabase</a> which we use at work.</p> <p>If you want to contribute, feel free to contact me.</p> <p>The plugin is open-source and Apache 2.0 licensed. Big thanks to <a href="https://simonwillison.net/">Simon Willison</a> and <a href="https://lalitm.com">Lalit Maganti</a> for doing the heavy lifting here.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[datasette-syntaqlite is a Datasette plugin that augments Datasette’s query editor using syntaqlite. Currently it catches syntax errors and incorrect columns based on available tables and views.]]></summary></entry><entry><title type="html">TIL: Deploying Datasette on fly.io</title><link href="https://theophilec.github.io/blog/2025/TIL-datasette-fly/" rel="alternate" type="text/html" title="TIL: Deploying Datasette on fly.io"/><published>2025-04-14T00:00:00+00:00</published><updated>2025-04-14T00:00:00+00:00</updated><id>https://theophilec.github.io/blog/2025/TIL-datasette-fly</id><content type="html" xml:base="https://theophilec.github.io/blog/2025/TIL-datasette-fly/"><![CDATA[<p>For a new open data project, I get data from an RSS feed and scraping and do structured extraction and summarization using LLMs. I use SQLite for storage, and <a href="https://datasette.io">Datasette</a> as a (temporary?) front-end.</p> <p>To share my intermediate progress with my client, I use <a href="https://fly.io">fly.io</a> hosting, with username authentication to access the data.</p> <p>Datasette has a very rich plugin ecosystem. I used two of them to reach this goal:</p> <ol> <li><a href="https://github.com/simonw/datasette-publish-fly">datasette-publish-fly</a>, to be used in conjuction with the <code class="language-plaintext highlighter-rouge">datasette publish</code>.</li> <li><a href="https://github.com/simonw/datasette-auth-passwords">datasette-auth-passwords</a>, for authentication. It allows to set username and passwords and create access control. If you only install the plugin and share a password hash, logging in will work but won’t be required to access the database. Access control configuration happens in <code class="language-plaintext highlighter-rouge">metadata.json</code> (see below or <a href="https://github.com/simonw/datasette-auth-passwords/issues/7">documentation</a>).</li> </ol> <p>Here is the basic <code class="language-plaintext highlighter-rouge">metadata.json</code> I used, which hides the database from everyone except logged-in users:</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"databases"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"&lt;database_name&gt;"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"allow"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div> <p>Then, I deployed with</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>uv run datasette publish fly &lt;database_name&gt;.db <span class="se">\</span>
            <span class="nt">--app</span><span class="o">=</span><span class="s2">"app_name"</span> <span class="se">\</span>
            <span class="nt">-m</span> metadata.json <span class="se">\</span>
            <span class="nt">--install</span> datasette-auth-passwords <span class="se">\</span>
            <span class="nt">--plugin-secret</span> datasette-auth-passwords &lt;username&gt;_password_hash <span class="s1">'pbkdf2_sha256...'</span>
</code></pre></div></div> <p>Above:</p> <ul> <li><code class="language-plaintext highlighter-rouge">&lt;username&gt;</code> is the desired user name (it should be replaced everywhere above).</li> <li><code class="language-plaintext highlighter-rouge">&lt;hash&gt;</code> is the hash of the password I chose. It is generated using the <code class="language-plaintext highlighter-rouge">echo "&lt;password&gt;" | uv run datasette hash-password --no-confirm</code> command provided by <a href="https://github.com/simonw/datasette-auth-passwords">datasette-auth-passwords</a>.</li> <li><code class="language-plaintext highlighter-rouge">&lt;database_name&gt;</code> is the name of the database we want to hide behind authentication:</li> </ul> <p>The login URL is: <code class="language-plaintext highlighter-rouge">https://&lt;app_name&gt;.fly.dev/-/login</code>.</p>]]></content><author><name></name></author><category term="llm"/><category term="datasette"/><summary type="html"><![CDATA[with password authentication]]></summary></entry><entry><title type="html">Mapping the French Culinary Network</title><link href="https://theophilec.github.io/blog/2025/foudinge/" rel="alternate" type="text/html" title="Mapping the French Culinary Network"/><published>2025-02-27T00:00:00+00:00</published><updated>2025-02-27T00:00:00+00:00</updated><id>https://theophilec.github.io/blog/2025/foudinge</id><content type="html" xml:base="https://theophilec.github.io/blog/2025/foudinge/"><![CDATA[<blockquote> <p><em>This post was shared by .txt on <a href="https://www.linkedin.com/posts/dottxt_heres-an-impressive-real-world-application-activity-7301322258201923584-01K5?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAACOfhn4Byev3cgLMB0m80WAJqy6BUm_-NM4">Linkedin</a> and <a href="https://bsky.app/profile/dottxtai.bsky.social/post/3ljb5a7hriq2y">Bluesky</a>, and discussed on <a href="https://news.ycombinator.com/item?id=43242818">Hacker News</a>.</em></p> </blockquote> <p>For French restaurant intel, <a href="https://lefooding.com">lefooding.com</a> is the definitive data source. Their anonymous critics conduct systematic reviews of establishments across France (and now Belgium), documenting their findings in a witty - if peculiar - style. Beyond choosing the place for a delicious night out, they can be used to map and understand France’s culinary network.</p> <p>A network is composed of nodes and edges. Nodes represent entities, such as people or restaurants. Edges represent the relationships between people and restaurants, or absence thereof. For the French restaurant scene, nodes represent both people and restaurants. A person node is connected to a restaurant if that person is known to have worked at the restaurant.</p> <p>Restaurants with very many neighbor nodes (nodes connected to it) are restaurants whose alumni go on to create and work in other prestigious restaurants. This is the case of <a href="https://www.ducasse-paris.com/">Ducasse</a> (ok, not a restaurant but a placeholder for all the restaurants in the Ducasse brand), <a href="https://www.mandarinoriental.com/fr">Mandarin Oriental</a> (technically the restaurant is called Sur Mesure) or <a href="https://www.septime-charonne.fr/">Septime</a>.</p> <p>To visualize and analyse this network, I combine information contained in reviews of restaurants across France by <a href="https://lefooding.com">LeFooding.com</a>’s critics. Most reviews contain information about the staff and their CV.</p> <p>Take this (shortened) review of <a href="https://lefooding.com/restaurants/grenat">Grenat</a> (emphasis mine):</p> <blockquote> <p>À l’enseigne de Grenat, <strong>Antoine Joannier</strong> et <strong>Neil Mahatsry</strong> ont le rouge aux joues et aux murs. <strong>Après avoir officié à La Brasserie Communale, où ils se sont rencontrés</strong>, les deux compères font désormais feu de tout bois en plein centre de Marseille, où <strong>Antoine veille au grain autour des tables en bois blond, convoyant les plats que Neil embrase à tout-va depuis l’âtre derrière le comptoir</strong>. Des huîtres aux pièces viandardes, […]</p> </blockquote> <p>and LeFooding.com’s English version:</p> <blockquote> <p>At Grenat, <strong>Antoine Joannier</strong> and <strong>Neil Mahatsry</strong> are bathed in an ardent red glow, much like the pomegranate-toned walls of their space. <strong>After working together at La Brasserie Communale, where they first met</strong>, the duo is now firing on all cylinders in the heart of Marseille, where <strong>Antoine tends to guests seated around blonde wood tables, delivering dishes ignited by Neil behind the bar.</strong> From oysters to prime cuts of red meat, […]</p> </blockquote> <p>Notice that the review mentions the restaurant staff, their prior experience at <em>La Brasserie Communale</em> and gives their roles in the restaurant. All of this information should be included in the network. Using OpenAI’s gpt4o-mini model with its structured generation functionality, I can extract information from the 1800 publicly available <a href="https://lefooding.com">LeFooding.com</a> reviews going back four years. Instead of the order of 1800 “current” staff-restaurant relationships, I uncovered over 5000 links, enriching our analysis with all possible information from the reviews.</p> <p>I combined the extracted information into a graph you can explore below (or <a href="https://ouestware.gitlab.io/retina/1.0.0-beta.6/#/graph/?url=https%3A%2F%2Fgist.githubusercontent.com%2Ftheophilec%2F351f17ece36477bc48438d5ec6d14b5a%2Fraw%2Ffa85a89541c953e8f00d6774fe42f8c4bd30fa47%2Fgraph.gexf&amp;r=v&amp;sa=re&amp;ca[]=t&amp;ca[]=ra-s">click here</a> below. To get started in exploring look for <em>Grenat</em>, Antoine, Neil or <em>Septime</em> and other restaurants I’ve mentioned. <sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p> <p>The network has over 5000 nodes and as many edges. In the visualization, green nodes represent staff and purple nodes represent restaurants. Larger nodes have a higher degree (more neighbors). Edges link restaurants and staff. You can it below. Look for your favorite restaurant, or click around to discover the connected components in the graph.</p> <iframe width="800" height="600" src="https://ouestware.gitlab.io/retina/1.0.0-beta.6/#/embed/?url=https%3A%2F%2Fgist.githubusercontent.com%2Ftheophilec%2F351f17ece36477bc48438d5ec6d14b5a%2Fraw%2Ffa85a89541c953e8f00d6774fe42f8c4bd30fa47%2Fgraph.gexf&amp;r=x&amp;sa=re&amp;ca[]=t&amp;ca[]=ra-s" frameborder="0" title="Retina" allowfullscreen=""></iframe> <h2 id="making-the-network">Making the network</h2> <p>To get here, I scraped data from <a href="https://lefooding.com">LeFooding.com</a>, extracted the desired information from the reviews, built the graph and finally made the visualization. The code used is available at <a href="https://github.com/theophilec/foudinge">theophilec/foudinge</a>.</p> <p>The crux of the approach is extracting the information is a structured way. Indeed, I need an easy was of building the graph and knowing which restaurants and people are related. Large Language Models (LLMs) are excellent in understanding a text, and can answer questions like “Where did Antoine Joannier work before Grenat?” with the Grenat review. However, if we ask for the information is a structured format like a JSON object, they struggle to reliably give useful answers.</p> <p>For instance, the desired output for the Grenat review could be something like this:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Person</span><span class="p">(</span>
  <span class="n">name</span><span class="o">=</span><span class="sh">'</span><span class="s">Antoine Joannier</span><span class="sh">'</span><span class="p">,</span>
  <span class="n">role</span><span class="o">=</span><span class="sh">'</span><span class="s">Host</span><span class="sh">'</span><span class="p">,</span>
  <span class="n">previous_restaurants</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">La Brasserie Communale</span><span class="sh">'</span><span class="p">]</span>
<span class="p">)</span>

<span class="nc">Person</span><span class="p">(</span>
  <span class="n">name</span><span class="o">=</span><span class="sh">'</span><span class="s">Neil Mahatsry</span><span class="sh">'</span><span class="p">,</span>
  <span class="n">role</span><span class="o">=</span><span class="sh">'</span><span class="s">Chef</span><span class="sh">'</span><span class="p">,</span>
  <span class="n">previous_restaurants</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">La Brasserie Communale</span><span class="sh">'</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div> <p>This is the art of <em>structured generation</em>.</p> <p>There are several approaches to this problem. First, one can <em>prompt better</em>. Second, and in combination with the first approach, one can retry inference until the response parses in the desired schema. While this can work as a one-off when hacking a configuration file, it won’t work for almost 2000 reviews.</p> <p>Another approach uses the logits of the model. These define the sampling distributions for the next token. They can be modified at inference to force the generated tokens to build into a response respecting a defined structure. Behind the scences, this relies on regular expressions and finite state machines (the same techniques used when <em>parsing</em> JSON). When using such techniques, you prompt the model however you want (and give the schema to inference API) and the output provably respects the desired structure 100% of the time, and can be used reliably downstream.</p> <p>The <code class="language-plaintext highlighter-rouge">outlines</code> library built by <a href="https://dottxt.co/">.txt</a> makes this possible for your favorite open model, such as <code class="language-plaintext highlighter-rouge">Mistral-7B-v0.3</code>. It also wraps OpenAI’s implementation of this in its API.</p> <p>Once a <a href="https://github.com/theophilec/foudinge/blob/main/foudinge/entities.py"><code class="language-plaintext highlighter-rouge">prompt_template</code></a> and <a href="https://github.com/theophilec/foudinge/blob/main/foudinge/entities.py"><code class="language-plaintext highlighter-rouge">Summary</code> schema</a> are designed, it’s as easy as:</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span> <span class="o">=</span> <span class="bp">...</span>
<span class="n">generator</span> <span class="o">=</span> <span class="n">outlines</span><span class="p">.</span><span class="n">generate</span><span class="p">.</span><span class="nf">json</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="n">Summary</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="nf">generator</span><span class="p">(</span><span class="nf">prompt_template</span><span class="p">(</span><span class="n">text</span><span class="p">))</span>
</code></pre></div></div> <p>I put Mistral, Meta and OpenAI head to head. Both Llama3.2-3B and Mistral-7B-v0.3 had a tendency to hallucinate fake people and related restaurants. In the end, I opted for OpenAI gpt4o-mini, which was also much faster. But even gpt4o-mini was susceptible to mistakes, which makes me optimistic that better prompting, an improved schema design will allow me to reproduce the results with Mistral or Llama (future work).</p> <p>The total cost of inference over 2000 reviews with gpt4o-mini with its structured generation endpoint is less that 1€!</p> <p>I used <a href="https://gephi.org/gephi-lite">gephi-lite</a> to create work on the visualization. The key step was spatialization, which defines a spatial layout of the graph that makes sense given the information contained in the graph. In our case, I used the force simulation setting which sends connected nodes close and nodes with larger degree to the center. The embedded visualization runs using WebGL thanks to the <a href="https://ouestware.gitlab.io/retina">Retina</a> project<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p> <p>In order to handle duplicates and eliminating some obvious errors visible on the graph once visualized I asked Claude 3.7 Sonnet to create a simple web app to allow me to edit the inferred entities while keeping the structure intact (as well as the encoding issues caused by sqlite/myself). It only took 2 iterations to get it working as I wanted, and then a few more iterations to get full-text search and some other features. It uses Flask and pure JS in jinja templates. It is available on Github at <a href="https://github.com/theophilec/foudinge-scrub"><code class="language-plaintext highlighter-rouge">foudinge-scrub</code></a> There are still a few duplicates to fix, which I’ll get around to (maybe).</p> <p>All the code used for scraping (using sqlite, asyncio and aiohttp), the inference (using outlines), the prompt and the Pydantic Schema are available on Github at <a href="https://github.com/theophilec/foudinge">theophilec/foudinge</a>.</p> <h3 id="conclusion">Conclusion</h3> <p>This project illustrates how LLMs can be used to extract information from rich sources of textual data, in this instance restaurant reviews. I am very happy with the tools I chose. A good next step is to get all this running of open models, locally or in a public cloud.</p> <p><em>Thanks to Adrien Bocquet and Clotilde Bukato for proofreading earlier versions of this post.</em></p> <div class="footnotes" role="doc-endnotes"> <ol> <li id="fn:1"> <p>Thanks to <a href="https://github.com/jacomyal">@jacomyal</a> from <a href="https://github.com/ouestware">@ouestware</a> on GitHub for helping me get a better spatialization. I have updated the visualization in the post. You can see the original and inferior spatialization <a href="https://ouestware.gitlab.io/retina/1.0.0-beta.4/#/graph/?url=https%3A%2F%2Fgist.githubusercontent.com%2Ftheophilec%2F351f17ece36477bc48438d5ec6d14b5a%2Fraw%2Ffa85a89541c953e8f00d6774fe42f8c4bd30fa47%2Fgraph.gexf&amp;r=x&amp;sa=re&amp;ca[]=t&amp;ca[]=ra-s&amp;st[]=u&amp;st[]=re&amp;ed=u">here</a>. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> <li id="fn:2"> <p>I identified and reported an <a href="https://github.com/gephi/gephi-lite/issues/187">issue</a> in gephi-lite where string escaping prevented proper Retina imports. It is now fixed in production. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p> </li> </ol> </div>]]></content><author><name></name></author><category term="llm"/><category term="food"/><summary type="html"><![CDATA[Creating a knowledge graph of restaurants featured in lefooding.com reviews using `outlines` and `gpt4o-mini`.]]></summary></entry><entry><title type="html">Animating SVGs with Claude 3.5 Sonnet</title><link href="https://theophilec.github.io/blog/2025/SVG-animation/" rel="alternate" type="text/html" title="Animating SVGs with Claude 3.5 Sonnet"/><published>2025-01-09T00:00:00+00:00</published><updated>2025-01-09T00:00:00+00:00</updated><id>https://theophilec.github.io/blog/2025/SVG-animation</id><content type="html" xml:base="https://theophilec.github.io/blog/2025/SVG-animation/"><![CDATA[<p>Following the Arxiv-submission of <a href="https://sagipolaczek.github.io/NeuralSVG/">NeuralSVG</a>, a HN commenter <a href="https://news.ycombinator.com/item?id=42638558">shared</a> their experiment animating an SVG using Claude 3.5 Sonnet.</p> <p>I tried with <a href="https://mines-paris.org">Mines Paris Alumni</a>’s SVG logo with Claude 3.5 Sonnet, online. One gotcha is that you must past the SVG contents instead of upload it.</p> <p>Here is the prompt:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Animate this SVG to make the text appear below. Make it very corporate and professional.
</code></pre></div></div> <p>and the initial logo :</p> <p><img src="/assets/img/20250109-initial-logo.svg" alt="Animated Mines Paris Alumni logo"/></p> <p>Here are the results (the second one was asked to be more “exciting”):</p> <style>
  .svg-container {
    animation: reset-animation 3.8s infinite; /* 3.8s matches the total animation time */
  }

  @keyframes reset-animation {
    0%, 99.999% { opacity: 1; }
    100% { opacity: 0.999; } /* Tiny opacity change triggers reset */
  }
</style> <div class="svg-container"> ![Animated Mines Paris Alumni logo](/assets/img/20250109-animated-logo.svg) </div> <p><img src="/assets/img/20250109-animated-logo-1.svg" alt="Animated Mines Paris Alumni logo"/></p> <p>Then, making it have flashing colours, it loses some text it cannot recover despite my coaxing (and the site says an output limit has been reached). <img src="/assets/img/20250109-animated-logo-2.svg" alt="Animated Mines Paris Alumni logo"/></p> <p>In any case, super exciting and fun to play with!</p>]]></content><author><name></name></author><category term="llm"/><category term="art"/><summary type="html"><![CDATA[Reproducing some ideas from HN.]]></summary></entry><entry><title type="html">TIL - configuring HTTPS for Github Pages and OVH</title><link href="https://theophilec.github.io/blog/2024/TIL-https-gh-pages/" rel="alternate" type="text/html" title="TIL - configuring HTTPS for Github Pages and OVH"/><published>2024-12-10T00:00:00+00:00</published><updated>2024-12-10T00:00:00+00:00</updated><id>https://theophilec.github.io/blog/2024/TIL-https-gh-pages</id><content type="html" xml:base="https://theophilec.github.io/blog/2024/TIL-https-gh-pages/"><![CDATA[<p><em>This TIL is following configuring verified domains for two of my websites hosted on Github Pages with HTTPS enabled.</em></p> <ol> <li>Remove default DNS CNAME and A entries in OVH.</li> <li>Add the A entries specified by GH Pages, and add TXT entries for domaine verification.</li> <li>Add a CNAME entry for <code class="language-plaintext highlighter-rouge">www.domain.com</code> going to the default GH pages domain, i.e. <code class="language-plaintext highlighter-rouge">username.github.io</code> or <code class="language-plaintext highlighter-rouge">organization.github.io</code>.</li> </ol> <p>Without the <code class="language-plaintext highlighter-rouge">www</code> subdomain entry, the DNS check fails and HTTPS cannot be enabled.</p>]]></content><author><name></name></author><category term="TIL"/><category term="web"/><summary type="html"><![CDATA[For future reference...]]></summary></entry><entry><title type="html">Chrome extension for logging attempts</title><link href="https://theophilec.github.io/blog/2024/giskard-logging/" rel="alternate" type="text/html" title="Chrome extension for logging attempts"/><published>2024-10-20T00:00:00+00:00</published><updated>2024-10-20T00:00:00+00:00</updated><id>https://theophilec.github.io/blog/2024/giskard-logging</id><content type="html" xml:base="https://theophilec.github.io/blog/2024/giskard-logging/"><![CDATA[<p>I just released a <a href="https://github.com/theophilec/giskard-red-challenge-helper">Chrome extension</a> for logging attempts at beating the Giskard RED Challenge. It is available on <a href="https://github.com/theophilec/giskard-red-challenge-helper">Github</a> under the MIT License. It is written in Javascript with the help of Anthropic’s Claude Sonnet.</p> <p>Giskard created a LLM red-teaming challenge called <a href="https://red.giskard.ai">Giskard RED</a>. The goal is to make a model misbehave or fail (e.g. output invalid SQL, get off-topic, …). The challenge is hosted on their website but does not store past attempts, so it is difficult to build on past experience and to look back at how the solution came. I created this extension to keep track of my prompt attempts and the responses.</p> <h2 id="what-it-does">What it does</h2> <p>By monitoring DOM updates, the extension captures the attempted prompt and response. These are stored with other metadata in persistent storage (IndexedDB).</p> <p>History can be viewed though a popup with a challenge selector. All stored data can be exported to JSON so it can be analysed or kept.</p> <p><img src="/assets/img/giskard-demo.png" alt="Demo image"/></p> <h2 id="coding-mdn--anthropics-claude-sonnet">Coding: MDN + Anthropic’s Claude Sonnet</h2> <p>Since I knew nothing about JS or browsers before, it was my first “non-trivial” project with help from models. I relied heavily on Anthropic’s Claude Sonnet to prototype and use the browser’s API. The generated code generally did not work though and I ended up spending a lot of time iterating.</p> <p>Several factors for this (which end up being the things I learned about JS and browsers):</p> <ul> <li>Claude can’t execute code (and in particular can’t test a browser extension against a live website) so it was short-sided and iterations were slow (and required a lot of clicking on my part)</li> <li>Browsers have strict and complex permission rules. I think the generated JS was generally fine but failed because permissions were deprecated (manifest v2 vs v3) or not respected (content vs background vs plugin…)</li> <li>Claude did not get a chance to watch the DOM update by inspecting the source code dynamically like I could. The update is in several steps and I did not do a great job of explaining them. It would be great to “record” a browser session to share the state.</li> <li>When scraping was not working, I tried intercepting the response from the model (which is not obfuscated) but this is difficult/not possible despite the model proposing it as a solution.</li> </ul> <p>Once I figured out how I could make it work and then I wrote the code myself. Once everything was working, the popup and export features were written in two iterations by Claude (it did not realize at first that only <code class="language-plaintext highlighter-rouge">popup.js</code> could not access the IndexedDB database directly, and solved the problem using messaging between <code class="language-plaintext highlighter-rouge">popup.js</code> and <code class="language-plaintext highlighter-rouge">content.js</code>).</p> <h2 id="conclusion">Conclusion</h2> <p>Once you get over the first hill, extensions are a powerful way of extending functionality online.</p>]]></content><author><name></name></author><category term="web"/><category term="llm"/><summary type="html"><![CDATA[Chrome extension for logging attempts in the Giskard RED Challenge.]]></summary></entry><entry><title type="html">PhD Defense</title><link href="https://theophilec.github.io/blog/2024/phd-defense/" rel="alternate" type="text/html" title="PhD Defense"/><published>2024-10-17T17:21:00+00:00</published><updated>2024-10-17T17:21:00+00:00</updated><id>https://theophilec.github.io/blog/2024/phd-defense</id><content type="html" xml:base="https://theophilec.github.io/blog/2024/phd-defense/"><![CDATA[<p>After three years at Inria Paris, I defended my PhD in machine learning yesterday in front of my jury, my advisors, my peers, my friends and family.</p> <p>My defense was recorded and is available on Youtube:</p> <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/5SiGh2v-st4?si=2ebniEm0s6Uhwk1f" 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=""></iframe> <p>The slides are <a href="https://docs.google.com/presentation/d/1iLGnYrVi_QfM9pvtkGa4WbgffMhWrzZGNJIcE2FFH34/edit?usp=sharing">here</a>. The combination of Google Slides + latex2png (in 800 dpi) was very efficient, especially for illustrations.</p> <p>My manuscript can be found <a href="https://drive.google.com/file/d/1Iwl_TPsCuMf-g0-mRiA5rf28V2gnBvMD/view?usp=drive_link">here</a>.</p> <p>My jury was composed of:</p> <ul> <li>Olivier Cappé, École Normal Supérieure (President and Examinateur)</li> <li>Gilles Blanchard, Université Paris Saclay (Rapporteur)</li> <li>Eric Moulines, École Polytechnique (Rapporteur)</li> <li>Lorenzo Rosasco, Università degli Studi di Genova (Examinateur)</li> <li>Zoltán Szabó, London School of Economics (Examinateur)</li> <li>Gersende Fort, Université de Toulouse (Examinatrice)</li> <li>Benjamin Guedj, Inria &amp; University College London (Directeur de thèse)</li> <li>Alessandro Rudi, Inria (Co-encadrant)</li> <li>Carlo Ciliberto, University College London (Co-encadrant)</li> </ul>]]></content><author><name></name></author><category term="phd"/><summary type="html"><![CDATA[I defended my PhD and recorded my presentation]]></summary></entry><entry><title type="html">Poiscaille</title><link href="https://theophilec.github.io/blog/2024/poiscaille/" rel="alternate" type="text/html" title="Poiscaille"/><published>2024-09-15T20:45:00+00:00</published><updated>2024-09-15T20:45:00+00:00</updated><id>https://theophilec.github.io/blog/2024/poiscaille</id><content type="html" xml:base="https://theophilec.github.io/blog/2024/poiscaille/"><![CDATA[<p>Le 11 septembre dernier, j’ai reçu un mail de <a href="https://www.poiscaille.fr">Poiscaille</a> annonçant les difficultés financières de Poiscaille, service que j’utilise depuis un peu plus d’un an (première commande le 29 août 2023).</p> <p>Poiscaille livre du poisson frais, pêché artisanalement en France dans les 72h avant la livraison, sur le mode de l’abonnement (mais également de la commande ponctuelle). La veille de sa livraison, le client choisit le contenu de son panier parmi une offre assez large de ce qui a été effectivement acheté à des pêcheurs. Si Poiscaille se compare au fonctionnement d’une Amap, l’abonnement est sans engagement.</p> <p>Les difficultés de Poiscaille ne sont pas si suprenantes. Le site avait déjà ajouté un système de bonus-malus sur le choix du produit : les produits “nobles” (et certainement les plus onéreux) comme le bar entier sont grévés d’un surcoût qui peut être significatif ; les produits “ordinaires” voire impopulaires comme le maquereau sont plus économiques avec un montant plus faible. Une manière de proposer les produits recherchés par une clientèle séduite par son modèle mais n’étant pas intéressée par des produits moins attractifs comme le silure (même si cette démographie est explicitement éloignée dans leurs <a href="https://poiscaille.zendesk.com/hc/fr/articles/6890542049053-Je-souhaite-acheter-uniquement-du-bar-des-daurades-royales-et-du-homard-pourquoi-je-ne-suis-pas-au-bon-endroit">FAQ</a>).</p> <p>Dans son mail, Poiscaille expose deux leviers pour l’aider : augmenter la fréquence de son abonnement (augmenter le panier moyen) et parraîner des proches (augmenter le nombre de clients). Poiscaille ne cherche pas à faire du crowdfunding, contrairement à ce que Kelbongoo souhaitait faire avant de se faire racheter.</p> <p>La vidéo partagée sur leur chaîne YouTube explique de nombreux points. Les commentaires rassemblent des fidèles du service ainsi que des naciens qui ont arrêté leur abonnement, regrettant notamment l’augmentation des prix du panier par rapport à son contenu.</p> <h2 id="mon-retour-dexpérience">Mon retour d’expérience</h2> <p>Nous sommes abonnés depuis août 2023, et avons découvert grâce aux parents de Clo, qui étaient déjà abonnés et fidèles clients. Nous ne mangeions jamais de poisson, à part lorsque nous étions chez eux. Plusieurs facteurs. Premièrement, aucune de nos lieux d’achat de courses ne propose de poisson sauvage, français et artisanal. Deuxièmement, nous avons des emplois du temps très irréguliers et n’allons pas chez le poissonier (ce que certains utilisateurs en commentaires de la vidéo Poiscaille proposent comme alternative), qui d’ailleurs incluent à la fois des produits d’élevage et étrangers.</p> <p>Utiliser Poiscaille était une manière de garantir des poissons et crustacés correspondant à nos attentes et nous obliger à en consommer. C’est toujours le cas aujourd’hui, et nous avons rarement été déçus.</p> <p>Côté rapport qualité-prix, nous dépensons 50 euros par mois chez Poiscaille. D’une part, cela reste une petite partie de notre budget alimentaire mensuel. D’autre part, <a href="http://riveline.net/poly/"><strong>le coût d’un bien n’existe pas</strong></a>, regardons les alternatives qui s’offrent à nous. Que proposent les poissonneries ? En cherchant quelques minutes, on voit qu’une <a href="https://www.ventetmaree.fr/DAURADE-ROYALE-DE-LIGNE-p290796205">daurade royale sauvage française et <em>a priori</em> artisanale</a> est vendue à 50 euros pièce et le maquereau vidé à <a href="https://www.ventetmaree.fr/MAQUEREAU-p296612862">20 euros le kilo</a>. On n’est pas loin – voire au-dessus – des prix Poiscaille. C’est sans compter la practicité, les garanties de qualité de Poiscaille et les garanties de revenus pour les pêcheurs.</p> <p>Poiscaille propose en ce moment une promotion et une offre de parainnage. Je recommande pleinement, allez-y !</p> <h2 id="chiffres-clés-et-des-ratios-un-peu-arbitraires-pour-réfléchir-à-voix-haute">Chiffres clés (et des ratios un peu arbitraires pour réfléchir à voix haute)</h2> <p>Poiscaille donne quelques chiffres clés. Qu’est-ce que cela nous apprend ?</p> <p>En septembre 2024, Poiscaille dit avoir:</p> <ul> <li>20 500 abonnés (cible : 25 000 abonnés)</li> <li>livrer 25 000 paniers par mois au total</li> <li>pour 10 000 000 euros de chiffre d’affaires annuel</li> <li>et 1 000 tonnes de marchandise par an.</li> </ul> <p>Quelques réflexions :</p> <ul> <li>nous achetons deux paniers par mois, donc sommes au dessus de la moyenne (1,22 paniers par abonné et par mois)</li> <li>le CA moyen par panier est de 33 euros, donc nous sommes en dessous de la moyenne en taille de casier (25 euros par mois) (je mélange probablement du HT et du TTC mais peu importe)</li> <li>Poiscaille génère 10 euros de chiffre d’affaire par kilo de marchandise. Cela paraît très peu pour des fruits de mer, surtout par rapport à la valeur moyen du casier. Mais certainement que les 1 000 tonnes sont brutes, avant vidage, découpe et conditionnement.</li> </ul> <p>Poiscaille explique avoir 70 salariés dont 25 au siège. Cela fait 45 salariés pour: acheter en région, transformer (l’étendu du conditionnement fait par Poiscaille n’est pas présenté) et expédier (livrer à Paris et confier au transporteur autrement). Pour 25 000 paniers par mois que je suppose également répartis sur les 20 jours ouvrés d’un mois, cela fait 1 000 paniers à conditionner par jour. Pour (admettons) 30 salariés, cela fait 300 paniers par personne et par jour soit 42 paniers par heure : cohérent.</p> <p>Poiscaille a publié ses derniers comptes en 2022 mais nous n’avons accès qu’au bilan et pas au compte de résultat. Les “calculs” ici se basent donc sur le site de Poiscaille et sur la vidéo de son fondateur. Le bilan montre quand même que l’entreprise possède des installations industrielles (ce qui est logique considérant son activité de conditionnement) et présente ses dettes, contractées pendant l’exercice de 2021 notamment (dernières données disponibles).</p> <h2 id="interrogations">Interrogations</h2> <p>Si quelqu’un de Poiscaille lit ceci, l’objectif était de consigner ce à quoi j’ai pensé en regardant votre vidéo. Je suis preneur de vos précisions sur deux points que je ne comprends pas (par ignorance de ma part):</p> <ul> <li>le ratio CA et quantité de marchandise,</li> <li>la nature des garanties offertes aux fournisseurs et le fonctionnement de votre marché (garanties de prix planchers alors que les prix s’envolent ?).</li> </ul>]]></content><author><name></name></author><category term="personal"/><summary type="html"><![CDATA[Poiscaille's financial difficulties (French).]]></summary></entry></feed>