<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Knowledge-Graphs - Gökhan Arkan</title><link>https://gokhanarkan.com/blog/topics/knowledge-graphs/</link><description>Software engineer at GitHub Copilot and postgraduate at Oxford CS. Writing about software engineering, AI, side projects, and productivity.</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>gokhan@hey.com (Gökhan Arkan)</managingEditor><webMaster>gokhan@hey.com (Gökhan Arkan)</webMaster><author>Gökhan Arkan</author><copyright>2026 Gökhan Arkan</copyright><atom:link href="https://gokhanarkan.com/blog/topics/knowledge-graphs/index.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Sat, 23 May 2026 08:17:58 +0000</lastBuildDate><item><title>Teaching Sommo to Reason: Adding a Knowledge Graph to a Wine Recommender</title><link>https://gokhanarkan.com/blog/sommo-knowledge-graph/</link><pubDate>Sat, 23 May 2026 00:00:00 +0000</pubDate><guid>https://gokhanarkan.com/blog/sommo-knowledge-graph/</guid><description>Sommo is already a capable wine LLM. I built a knowledge graph to test whether it could meaningfully reduce hallucinations and improve structured recommendations. Datalog rules, ComplEx embeddings, and a measured 3.5x drop in invented wineries.</description><content:encoded>&lt;![CDATA[<p>A few months ago I shipped Sommo v1, a fine-tuned 7B model that knows wine. Sommo v2 is the private model that powers the iOS app. For its size, it is genuinely strong in the wine niche, but only because a lot of disciplined LLM work sits behind it: a custom evaluation harness with domain-specific test sets, regression tests on previous failures, careful prompt and training-data curation, calibration against expert wine references. v2 is the LLM-only ceiling more or less reached.</p><p>But every language model, no matter how well-tuned, will occasionally invent a winery, misattribute a grape variety, or confidently recommend something that does not exist. Evals catch the gross failures. The subtle ones (an almost-real producer name, a vintage just out of range) are exactly what slips through judgment-based testing. That is the gap I wanted to close.</p><p>I ran this work at the University of Oxford as a water-test: a small, controlled experiment to see whether grounding language models in structured knowledge was worth pursuing at a larger scale. Sommo was the natural test subject. I already had the model and the use case.</p><p>The hypothesis: a knowledge graph could fix the residual hallucinations. Not in a hand-wavy &ldquo;structured data is good&rdquo; way, but in a concrete, measured way. So I built one, tested it on the same prediction tasks two different ways (logic rules and ML embeddings), and ran the LLM with and without the KG as context.</p><p>This post walks through what I did, what I measured, and what I would do differently. The full technical write-up is available as a<a href="https://gokhanarkan.com/papers/wine-knowledge-graph.pdf" target="_blank" rel="noopener noreferrer">paper (PDF)</a>
if you want the formal version with all the tables, and the<a href="https://github.com/gokhanarkan/wine-knowledge-graph" target="_blank" rel="noopener noreferrer">source code is on GitHub</a>
.</p><h2 id="why-even-bother">Why even bother</h2><p>The honest answer: language models are pattern completers, not databases. Sommo v1 hallucinates. The blog post for v1 said this explicitly. Telling users &ldquo;the model might invent things&rdquo; is fine for a hobby project. For an app people pay for, the right answer is to make the model invent fewer things.</p><p>There are two standard moves:</p><ol><li><strong>More training data.</strong> Diminishing returns, expensive, and you cannot retrain on every new producer.</li><li><strong>Retrieval-augmented generation.</strong> Plug the model into a verified data source so it can ground its answers.</li></ol><p>A knowledge graph is the second move done properly. Instead of stuffing raw text snippets into the prompt, you give the model structured facts: this winery exists, it is in this region, this region is in this country, this wine uses this variety, these wines are similar to that one for these reasons.</p><p>Knowledge graphs also let you do something language models cannot:<strong>deductive reasoning</strong>. If a region is in a province, and a province is in a country, then the region is in the country. A small Datalog program captures this in three lines. A 7B model needs millions of examples to approximate it, and even then it will get edge cases wrong.</p><p>I wanted to test whether this theoretical advantage actually shows up in measurements. Spoiler: it does, in some places more than others.</p><h2 id="the-dataset">The dataset</h2><p>I started with the<a href="https://www.kaggle.com/datasets/zynicide/wine-reviews" target="_blank" rel="noopener noreferrer">WineEnthusiast 130k reviews collection on Kaggle</a>
. Flat CSV. One row per review, with columns for country, province, region, winery, variety, points, price, taster, and the review text itself.</p><p>After filtering to a tractable slice (France, Italy and Spain; points &gt;= 85; non-null price/region/winery/variety) I had:</p><ul><li>34,189 wines</li><li>6,181 distinct wineries (after fuzzy-canonicalising names like<em>Château Latour</em> vs<em>Chateau Latour</em>)</li><li>363 grape varieties</li><li>806 named regions</li><li>29 provinces</li><li>3 countries</li></ul><p>Big enough that the geographic hierarchy is interesting, small enough that everything trains in minutes. Critically, the source is<strong>not</strong> a knowledge graph. It is a CSV. I had to build the graph myself, which was the point. Reusing Wikidata or DBpedia would have proven nothing.</p><h2 id="constructing-the-graph">Constructing the graph</h2><p>The pipeline is four idempotent stages, all driven from one set of pandas-emitted parquet tables:</p><ol><li><strong>Filter and normalise.</strong> Strip accents, lower-case, remove the boilerplate prefixes (<em>Château</em>,<em>Domaine</em>,<em>Tenuta</em>,<em>Bodegas</em>,<em>Cantina</em>,<em>Maison</em>,<em>Casa</em>), then fuzzy-cluster winery strings with<code>rapidfuzz</code> token-set ratio &gt;= 92. The 6,958 raw winery strings collapse to 6,181 canonical entities. Vintages come from a regex on the title; there is no vintage column in the source data.</li><li><strong>Bucket continuous attributes.</strong> Price into bands (<code>&lt;15</code>,<code>15-30</code>,<code>30-60</code>,<code>60-120</code>,<code>&gt;120</code>); points into quality tiers (<code>Good</code>,<code>VeryGood</code>,<code>Outstanding</code>,<code>Classic</code>).</li><li><strong>Emit entities and edges.</strong> One parquet per node type, one per edge type, surrogate IDs derived from sha1 hashes for stability.</li><li><strong>Dual-load.</strong> Push the same data into Neo4j (property graph, used for ad-hoc Cypher) and into RDF (N-triples, used by the logic engine). 41,623 nodes and 232,263 relationships in Neo4j; 418,187 triples in RDF, all consistent.</li></ol><p>The schema is small but expressive. Nine relation types:<code>producedBy</code>,<code>madeFrom</code>,<code>fromRegion</code>,<code>inProvince</code>,<code>inCountry</code>,<code>reviewedBy</code>,<code>hasVintage</code>,<code>hasPriceBand</code>,<code>hasQualityTier</code>. Plus a derived recursion target,<code>locatedIn</code>, declared as<code>owl:TransitiveProperty</code>.</p><p>That last property is the whole point of using RDF here: a recursive rule fills in the geographic closure automatically.</p><h2 id="the-two-solvers">The two solvers</h2><p>To make the comparison meaningful I defined two tasks both solvers had to attack:</p><ul><li><strong>Task A</strong>, link prediction: given a query wine, return the top 10 wines it is most similar to. Gold positives = same variety, same province, points within +/-2, price within 25%.</li><li><strong>Task B</strong>, KG completion: mask 5% of<code>madeFrom</code> edges; predict the held-out variety from the rest of the wine&rsquo;s structure.</li></ul><p>A shared evaluation harness drives any solver implementing the same<code>Solver</code> Protocol. Both solvers see identical splits. Same metrics (Hits@1, Hits@3, Hits@10, MRR). No moving the goalposts.</p><h3 id="logic-solver-datalog-rules">Logic solver: Datalog rules</h3><p>Five rules in a<code>.dl</code> file, evaluated by a small fixpoint engine I wrote on top of pandas. The interesting ones:</p><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-prolog" data-lang="prolog"><span class="line"><span class="cl"><span class="c1">% R1 (recursive): transitive geographic closure.</span></span></span><span class="line"><span class="cl"><span class="nf">locatedIn</span><span class="p">(</span><span class="nv">R</span><span class="p">,</span><span class="nv">P</span><span class="p">)</span><span class="p">:-</span><span class="nf">inProvince</span><span class="p">(</span><span class="nv">R</span><span class="p">,</span><span class="nv">P</span><span class="p">).</span></span></span><span class="line"><span class="cl"><span class="nf">locatedIn</span><span class="p">(</span><span class="nv">P</span><span class="p">,</span><span class="nv">C</span><span class="p">)</span><span class="p">:-</span><span class="nf">inCountry</span><span class="p">(</span><span class="nv">P</span><span class="p">,</span><span class="nv">C</span><span class="p">).</span></span></span><span class="line"><span class="cl"><span class="nf">locatedIn</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span><span class="nv">Z</span><span class="p">)</span><span class="p">:-</span><span class="nf">locatedIn</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span><span class="nv">Y</span><span class="p">),</span><span class="nf">locatedIn</span><span class="p">(</span><span class="nv">Y</span><span class="p">,</span><span class="nv">Z</span><span class="p">).</span></span></span><span class="line"><span class="cl"/></span><span class="line"><span class="cl"><span class="c1">% R2 (creates new edges): recommend a similar wine.</span></span></span><span class="line"><span class="cl"><span class="nf">recommend</span><span class="p">(</span><span class="nv">W1</span><span class="p">,</span><span class="nv">W2</span><span class="p">)</span><span class="p">:-</span></span></span><span class="line"><span class="cl"><span class="nf">sharesVariety</span><span class="p">(</span><span class="nv">W1</span><span class="p">,</span><span class="nv">W2</span><span class="p">),</span></span></span><span class="line"><span class="cl"><span class="nf">sameProvince</span><span class="p">(</span><span class="nv">W1</span><span class="p">,</span><span class="nv">W2</span><span class="p">),</span></span></span><span class="line"><span class="cl"><span class="nf">sharesPriceBand</span><span class="p">(</span><span class="nv">W1</span><span class="p">,</span><span class="nv">W2</span><span class="p">),</span></span></span><span class="line"><span class="cl"><span class="nf">sharesQualityTier</span><span class="p">(</span><span class="nv">W1</span><span class="p">,</span><span class="nv">W2</span><span class="p">),</span></span></span><span class="line"><span class="cl"><span class="nv">W1</span><span class="p">!</span><span class="o">=</span><span class="nv">W2</span><span class="p">.</span></span></span><span class="line"><span class="cl"/></span><span class="line"><span class="cl"><span class="c1">% R5: a winery is "premium" if it has at least 3 wines</span></span></span><span class="line"><span class="cl"><span class="c1">% in the Outstanding or Classic quality tier.</span></span></span><span class="line"><span class="cl"><span class="nf">premiumWinery</span><span class="p">(</span><span class="nv">Y</span><span class="p">)</span><span class="p">:-</span></span></span><span class="line"><span class="cl"><span class="nf">producedBy</span><span class="p">(</span><span class="nv">W</span><span class="p">,</span><span class="nv">Y</span><span class="p">),</span></span></span><span class="line"><span class="cl"><span class="nf">hasQualityTier</span><span class="p">(</span><span class="nv">W</span><span class="p">,</span><span class="nv">Q</span><span class="p">),</span></span></span><span class="line"><span class="cl"><span class="nv">Q</span><span class="s">in</span><span class="p">{</span><span class="s">qt_outstanding</span><span class="p">,</span><span class="s">qt_classic</span><span class="p">},</span></span></span><span class="line"><span class="cl"><span class="nf">count</span><span class="p">(</span><span class="nv">W</span><span class="p">)</span><span class="o">&gt;=</span><span class="mf">3.</span></span></span></code></pre></div><p>The<code>locatedIn</code> closure converges in two iterations from 835 base edges to 1,641 derived pairs. R2 materialises 2.6 million<code>similarTo</code>-style edges across the whole graph. R5 derives 247 premium wineries (Zind-Humbrecht, Louis Jadot, Leflaive, Latour, Louis Roederer at the top). All of this in 8.2 seconds end-to-end.</p><p>These are not facts I had to put in. They are facts that follow from facts I put in.</p><h3 id="ml-solver-complex-embeddings">ML solver: ComplEx embeddings</h3><p>For the machine-learning side I trained a<a href="https://arxiv.org/abs/1606.06357" target="_blank" rel="noopener noreferrer">ComplEx</a>
knowledge-graph embedding with<a href="https://github.com/pykeen/pykeen" target="_blank" rel="noopener noreferrer">PyKEEN</a>
. Embedding dim 128 (256 real components), 100 epochs of LCWA training with negative sampling, Adam at 1e-3, batch size 512, seed pinned. Final training loss 0.006.</p><p>Why ComplEx? Complex-valued embeddings handle asymmetric relations natively.<code>producedBy</code>,<code>madeFrom</code>,<code>fromRegion</code> are all asymmetric. TransE would struggle.</p><p>The model exposes nine relation types over 41,620 entities, totalling 230,554 training triples. The 1,709<code>madeFrom</code> edges that constitute Task B&rsquo;s test set were masked from training to keep the evaluation honest.</p><p>For Task A, the solver scores candidate wines by cosine similarity in the learned embedding space. For Task B, it uses PyKEEN&rsquo;s<code>score_t</code> to rank all varieties for a given wine.</p><h2 id="what-the-numbers-said">What the numbers said</h2><table><thead><tr><th>Solver</th><th>Task</th><th>Hits@1</th><th>Hits@3</th><th>Hits@10</th><th>MRR</th></tr></thead><tbody><tr><td>random</td><td>A</td><td>0.0001</td><td>0.0003</td><td>0.0005</td><td>0.0002</td></tr><tr><td>random</td><td>B</td><td>0.0018</td><td>0.0047</td><td>0.0222</td><td>0.0055</td></tr><tr><td>logic</td><td>A</td><td>0.0308</td><td>0.0769</td><td>0.1831</td><td>0.0673</td></tr><tr><td>logic</td><td>B</td><td>0.6975</td><td>0.9427</td><td>0.9994</td><td>0.8213</td></tr><tr><td>ml</td><td>A</td><td>0.0001</td><td>0.0005</td><td>0.0016</td><td>0.0004</td></tr><tr><td>ml</td><td>B</td><td>0.0919</td><td>0.2264</td><td>0.5073</td><td>0.1983</td></tr></tbody></table><p>A few things jump out.</p><p><strong>On Task B (variety prediction), logic wins overwhelmingly.</strong> Hits@1 of 0.70, against a random baseline of 1-in-363. The reason is structural: variety is essentially a deterministic function of (winery, province) for the majority of wines in this slice. A five-line Datalog program captures this exactly. ML reaches Hits@10 = 0.51, which is impressive given it has to learn the same pattern from triples without being told the rule, but still clearly second-best.</p><p><strong>On Task A (similarity), logic also wins,</strong> but the gap is more interesting. Logic gets Hits@10 = 0.18, a 367x improvement over random. The cap is set by the harness (<code>k = 10</code>), not by the solver&rsquo;s recall. The recommend-set has thousands of candidates per anchor wine, and presenting only ten of them is naturally lossy. ML, surprisingly, does worse than random on this one. The reason became clear when I dug in: ComplEx similarity scores triple plausibility, not entity proximity in any sense aligned with the gold definition. A learned siamese head trained directly on similarTo edges would close the gap.</p><p>To make the contrast concrete, I sampled 200 Task B wines and bucketed them by which solver got the gold variety:</p><table><thead><tr><th>Bucket</th><th>Count (n=200)</th></tr></thead><tbody><tr><td>Both correct</td><td>15</td></tr><tr><td>Logic only</td><td>129</td></tr><tr><td>ML only</td><td>3</td></tr><tr><td>Both wrong</td><td>53</td></tr></tbody></table><p>129 vs 3. Logic dominates the disagreement set by 43x. The &ldquo;both wrong&rdquo; bucket is where the interesting failures live, typically rare varieties (a Sangiovese clone called Prugnolo Gentile shows up here, almost no training signal).</p><h2 id="where-ml-earns-its-keep">Where ML earns its keep</h2><p>The numbers above make ML look like a worse logic solver. But there are two things logic cannot do that the embeddings handle naturally.</p><p>First,<strong>soft generalisation</strong>. The Prugnolo Gentile case is informative: ComplEx places the wine close in vector space to other Tuscan reds. The top-1 prediction is wrong (it picks Aglianico) but it is wrong in an interesting way. It has learned that this is some kind of rich southern Italian variety. The five-line Datalog program has no such fallback; it either knows the variety from the winery-province pattern or it does not.</p><p>Second,<strong>the embeddings are reusable</strong>. Once trained, the same vectors can be queried for any nearest-neighbour task: similar wines, similar wineries, similar regions, anomaly detection. The Datalog rules are bespoke to each predicate.</p><p>The honest read:<strong>logic and ML are complementary, not competitive</strong>. ML can teach logic where its thresholds are wrong (R4&rsquo;s &ldquo;characteristic variety&rdquo; cutoff of 50 is too coarse for low-volume varieties, which is exactly where ML&rsquo;s confident wrong answers cluster). Logic can repair ML by rejecting predictions that violate hard constraints (a Spanish-only variety predicted for a Bordeaux wine is a Datalog query, not a learned property).</p><h2 id="the-part-that-actually-matters-llm-grounding">The part that actually matters: LLM grounding</h2><p>This is the experiment I cared about most.</p><p>I picked 20 query wines and asked Gemini Flash to recommend three similar wines for each, in two conditions:</p><ul><li><strong>Unaided.</strong> Just the query wine, no context.</li><li><strong>Grounded.</strong> The same prompt, plus a list of eight candidate wines pulled from the KG (the logic solver&rsquo;s top recommendations).</li></ul><p>Then I checked every winery the model named against the canonical winery list from the KG, normalising both sides for accent stripping and prefix removal.</p><table><thead><tr><th>Condition</th><th>Wineries named</th><th>Hallucinated</th><th>Rate</th></tr></thead><tbody><tr><td>Unaided</td><td>60</td><td>16</td><td>26.7%</td></tr><tr><td>Grounded</td><td>52</td><td>4</td><td>7.7%</td></tr></tbody></table><p><strong>A 3.5x reduction in hallucinated wineries</strong> with no change to the model. Same prompt template, same temperature, same model version. The only difference was eight lines of KG-supplied candidate wines.</p><p>This is the standard RAG result, demonstrated against this KG and this candidate set. It is also the answer to &ldquo;should Sommo v3 use a knowledge graph?&rdquo; The answer is yes. The question now is engineering: real-time vector search over the candidate set, latency budget on the iOS round-trip, what to do when the KG returns nothing useful.</p><h2 id="the-reverse-direction-llm-enriches-kg">The reverse direction: LLM enriches KG</h2><p>The graph has zero edges for tasting notes. The CSV has descriptions like<em>&ldquo;This bright, juicy red shows ripe black cherry, leather and a hint of cinnamon, with firm but ripe tannins.&rdquo;</em> All of that vocabulary is wasted on a structured pipeline.</p><p>So I asked Gemini to extract tasting descriptors from 30 wine descriptions. It returned 180 descriptor tuples drawn from 138 unique terms. Top descriptors:<em>leather, spice, mineral, wood, toast, cinnamon, acidity, pineapple, juicy</em>. Every one of them is a candidate new edge:<code>Wine -hasTastingNote-&gt; Descriptor</code>.</p><p>This is the cleanest cooperation pattern I have seen between LLMs and KGs. The LLM does what the structured pipeline cannot (read free text). The KG does what the LLM cannot (verify that named entities actually exist). The two enrich each other in directions that play to their strengths.</p><h2 id="what-i-would-do-differently">What I would do differently</h2><p>A few things I would change for v2 of this experiment:</p><p><strong>Train ML on the actual prediction task.</strong> I used ComplEx similarity for Task A because it was easy. A learned siamese head trained directly on the similarTo edges materialised by R2 would almost certainly outperform logic on Task A, by exploiting the soft generalisation that pure cosine similarity throws away.</p><p><strong>Push more relations into the graph.</strong> The current schema is geographic plus quality plus price. Adding terroir (soil, climate, altitude), winemaking technique (oak ageing, fermentation vessel), and tasting profile (the LLM-extracted descriptors above) would give both solvers more to work with. It would also test whether logic&rsquo;s dominance on Task B holds up in a less structurally-loaded setting.</p><p><strong>Move from offline batches to live queries.</strong> Right now the logic engine pre-materialises 2.6 million<code>recommend</code> edges. For a real recommender this is wasteful. Most queries touch a tiny fraction. A query-time evaluator over a smaller derived index would scale much further.</p><p><strong>Build the actual retrieval layer.</strong> The grounding experiment used eight pre-computed candidates. The production version needs ANN search over wine embeddings, with filters for budget, region, and varietal preferences. Standard infrastructure, but the KG decides what gets indexed.</p><h2 id="try-it-yourself">Try it yourself</h2><p>Everything is open. The full pipeline reproduces from a clean machine via Docker:</p><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/gokhanarkan/wine-knowledge-graph</span></span><span class="line"><span class="cl"><span class="nb">cd</span> wine-knowledge-graph</span></span><span class="line"><span class="cl"/></span><span class="line"><span class="cl"><span class="c1"># Download winemag-data-130k-v2.csv from</span></span></span><span class="line"><span class="cl"><span class="c1"># https://www.kaggle.com/datasets/zynicide/wine-reviews</span></span></span><span class="line"><span class="cl"><span class="c1"># and place it at data/raw/</span></span></span><span class="line"><span class="cl"/></span><span class="line"><span class="cl">make up<span class="c1"># Neo4j + Python container</span></span></span><span class="line"><span class="cl">make smoke<span class="c1"># verify deps + dataset</span></span></span><span class="line"><span class="cl">make prep-data<span class="c1"># filter, normalise, emit entity tables</span></span></span><span class="line"><span class="cl">make build-kg<span class="c1"># load both stores; render figure</span></span></span><span class="line"><span class="cl">make build-splits</span></span><span class="line"><span class="cl">make logic-derive</span></span><span class="line"><span class="cl">make eval-logic-A<span class="o">&amp;&amp;</span> make eval-logic-B</span></span><span class="line"><span class="cl">make ml-install</span></span><span class="line"><span class="cl">make ml-triples<span class="o">&amp;&amp;</span> make ml-train</span></span><span class="line"><span class="cl">make eval-ml-A<span class="o">&amp;&amp;</span> make eval-ml-B</span></span><span class="line"><span class="cl">make compare<span class="c1"># comparative analysis tables</span></span></span></code></pre></div><p>All deterministic seeds are pinned to 42. The whole thing fits on a laptop.</p><h2 id="what-this-means-for-sommo">What this means for Sommo</h2><p>Sommo v1 was a fine-tuned model. Sommo v2 added proprietary training data, MCP connections, and the eval discipline that pushed accuracy as far as LLM-only techniques will take it. Sommo v3, when it ships, will pair the model with a knowledge graph. Not because graphs are fashionable, but because the numbers say so.</p><p>The 26.7% to 7.7% hallucination drop is the headline. The variety-prediction Hits@1 of 0.70 is the proof that even a five-line logic program can outclass a 7B language model on the right kind of structured task. The lesson is not &ldquo;abandon LLMs&rdquo;. The lesson is &ldquo;give them something solid to stand on&rdquo;.</p><p>Wine is just the test domain. The same pattern (LLM for natural language, KG for structured truth, each compensating for the other&rsquo;s failure modes) applies anywhere you care whether the names your model speaks are real.</p><hr><h2 id="urls-in-this-post">URLs in this post</h2><ul><li><a href="https://gokhanarkan.com/papers/wine-knowledge-graph.pdf" target="_blank" rel="noopener noreferrer">Paper (PDF)</a></li><li><a href="https://github.com/gokhanarkan/wine-knowledge-graph" target="_blank" rel="noopener noreferrer">Source code</a></li><li><a href="https://www.kaggle.com/datasets/zynicide/wine-reviews" target="_blank" rel="noopener noreferrer">Wine reviews dataset on Kaggle</a></li><li><a href="https://github.com/pykeen/pykeen" target="_blank" rel="noopener noreferrer">PyKEEN</a></li><li><a href="https://arxiv.org/abs/1606.06357" target="_blank" rel="noopener noreferrer">ComplEx paper</a></li><li><a href="https://apps.apple.com/app/sommo/id6757319027" target="_blank" rel="noopener noreferrer">Sommo on the App Store</a></li><li><a href="https://sommo.app" target="_blank" rel="noopener noreferrer">Sommo website</a></li><li><a href="/blog/sommo-7b-v1">Earlier post: Building a Sommelier in a Weekend</a></li></ul>
]]></content:encoded><category>machine-learning</category><category>llm</category><category>knowledge-graphs</category></item></channel></rss>