<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Yoav Vollansky on Medium]]></title>
        <description><![CDATA[Stories by Yoav Vollansky on Medium]]></description>
        <link>https://medium.com/@yoavxyoav?source=rss-892279cf5e14------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*qsb-pKdBjO45H_jA</url>
            <title>Stories by Yoav Vollansky on Medium</title>
            <link>https://medium.com/@yoavxyoav?source=rss-892279cf5e14------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 12:26:44 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@yoavxyoav/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Let It Go: You’re Not Getting Paid To Write Code Anymore]]></title>
            <link>https://tech.healthee.com/let-it-go-youre-not-getting-paid-to-write-code-anymore-20cfb9103068?source=rss-892279cf5e14------2</link>
            <guid isPermaLink="false">https://medium.com/p/20cfb9103068</guid>
            <category><![CDATA[ai-agent]]></category>
            <category><![CDATA[claude-code]]></category>
            <category><![CDATA[agents]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[coding]]></category>
            <dc:creator><![CDATA[Yoav Vollansky]]></dc:creator>
            <pubDate>Wed, 01 Apr 2026 15:57:10 GMT</pubDate>
            <atom:updated>2026-04-04T19:22:49.727Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*37i7LLMyYhJVr7zrgGaKwA.png" /></figure><p>A few weeks ago, I published a <a href="https://marketplace.visualstudio.com/items?itemName=YoavVollansky.logo-to-the-people">VSCode</a>/<a href="https://open-vsx.org/extension/yoavvollansky/logo-to-the-people#review-details">Cursor</a> extension on a whim. It detects and displays the project’s logo in your editor’s status bar and explorer panel. Not a world-changing technology, but a small quality-of-life widget that I wanted but never had the time (or honestly, the TypeScript skills) to build.</p><p>I’m not a frontend developer. I’m not a TypeScript developer. I’m a data scientist and AI engineer who spends most of his time with Python, modeling domain knowledge to data, creating pipelines, and in recent times, mostly building LLM-powered systems. Stuff like that were firmly in someone else’s jurisdiction (of the kind of thing you’d open a ticket for, wait a sprint, and then accept a someone else’s version of what you actually wanted). So I let go.</p><h3>A Timeline of Concessions</h3><p>There’s a history here of which the LLM revolution is just the visible tip. Code has been slowly, methodically stolen from us, and we’ve been in denial about it the whole time.</p><p>It started harmless, with books. O’Reilly manuals, thick as bricks, that encoded the knowledge of the craft. You didn’t need to know everything if you had the right shelf. Knowledge moved from the heads of experts into something accessible, transferable.</p><p>Then came blogs. Stack Overflow. The knowledge became <em>searchable</em>. You didn’t even need to read the book anymore — you needed to know how to use Google. The expert’s thought process, compressed into a forum thread, where you can also inflict vengeance for bad answers by other mere humans. Stealing someone else’s algo was common practice.</p><p>Then came ChatGPT. Now a real tectonic plate shifted. The loop</p><blockquote>problem → search → read → adapt → paste→ refactor</blockquote><p>collapsed into a single conversation. “How do I do X in TypeScript?” became a dialogue, not a scavenger hunt.</p><p>GitHub Copilot and similar tools took the next slice. Now you didn’t even need to finish your thought and the code already met you halfway (or even just right after you wrote the function name). The boilerplate and the obvious implementation just magically conjured itself in front of you.</p><p>And then Cursor arrived. And then Claude Code. And the agentic workflows. We know where we are today. That’s when the jurisdiction didn’t just shrink — it <em>collapsed</em>.</p><h3>So Now What’s New?</h3><p>Each handover felt uncomfortable. Each one was met with some version of: <em>“But you need to understand the code you’re writing.”</em> And each time, that was partially true, until it wasn’t. Until the next layer arrived, and the previous anxiety looked odd. If you have co-workers that insist they know every piece of code they wrote by heart, they are either lying or just terribly unproductive.</p><p>What’s left in your jurisdiction today is: the <em>what</em> and the <em>why</em>. Not the <em>How</em>. The domain knowledge. The product sense. The architectural judgment. The ability to look at a system and ask: <em>what problem are we actually solving here?</em></p><p>That’s not a small thing. That’s everything!</p><h3>The Hardest Part Isn’t the Code</h3><p>The good thing is that’s not new. But still, it seems like <strong>letting go of the code is psychologically hard</strong>.</p><p>For engineers especially, coding is identity. The ability to sit down, understand a system at the function level, and craft a solution from scratch, is what the industry trained us to value. It’s what interviews test. It’s what the senior title rewards. It’s what separates the “real engineers” from everyone else. <strong>We are not that far from the day that even the first part of tackling a problem — which is simply understanding it — will be taken out of our jurisdiction as well.</strong></p><p>Watching an AI write your code while you describe what you want feels, the first few times, like cheating. Like you’re not <em>really</em> building it and someone will find out. Imposter syndrome from your first post-graduation job is back to hunt you down. People used to (and still) chuckle apologetically when they <em>vibe-coded </em>something, a word that used to be almost derogatory. This term is <a href="https://x.com/karpathy/status/1886192184808149383?lang=en">very 2025</a>, and <a href="https://x.com/karpathy/status/2026731645169185220">it died</a> <a href="https://x.com/karpathy/status/2015883857489522876">way quicker</a> than expected. If you know people who are still using it to tease their colleagues, safely tell them they are boomers both in humor and in terms of AI technology adoption.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*fPskDsLrcuE5Qc_vH9qG1A.png" /><figcaption>The Vibe-Coder’s New Clothes</figcaption></figure><p>But here’s the thing: nobody finds out, because there’s nothing to find out. The thing works. The users don’t care how it was built. The code is reviewed, the tests pass, the logic is sound. The question of whether <em>you personally</em> typed the curly braces is a question that was already becoming irrelevant. We’ve just finally crossed the threshold where it’s obvious.</p><p>We stopped being coders a long time ago. When we started copy-pasting from Stack Overflow, we became <em>assemblers</em>. When Copilot arrived, we became <em>directors</em>. Now, with agentic workflows, we’re becoming something new again. And the transition, as always, is uncomfortable for the people who built their identity on the previous layer.</p><p>Don’t get me wrong — the code your agent produces is your code and your responsibility. You still need to make sure good tests are included and that it works. You need to make sure there are no potential secutirty disasters (or just normal disasters because you didn’t even have a quick glance we quick glances are important). This all depends on your product and how critial its functioning is. If you wrote software for Artemis II is one thing, if it’s a scraper that is doom to fail anyway sometimes — it is another.</p><h3>What a Data Scientist Can Build Now</h3><p>Let me be concrete about what changed for me.</p><p>Before, building a VSCode extension required: learning the extension API, understanding the TypeScript ecosystem, knowing how to structure a VS Code project, debugging the activation events, figuring out the publish workflow, handling the manifest, the icons, the marketplace metadata. It required, at minimum, a weekend of reading documentation and probably a few frustrated Stack Overflow tabs.</p><p>Now? I described what I wanted. I approved a plan. I reviewed the components at a high level. I made decisions about behavior and the implementation happened <em>around</em> those decisions. I still read the code and I still caught issues. I definitely made architectural calls. For now, we still have <em>that</em> layer (but don’t get used to it).</p><h3>The Problem That Proved the Point</h3><p>I needed this extension because my project count exploded. Two years ago I was juggling two, maybe three projects at once — the normal load of a data scientist with a roadmap and a backlog.</p><p>But now, every spark of an idea could become a POC in hours. Every POC could reach production in days. The pipeline from <em>I wonder if we could…</em> to <em>here’s a working demo</em> compressed from weeks to an afternoon. And all in parallel. Long-term initiatives I’d been pushing forward in spare moments started moving fast. New ideas stopped dying on the whiteboard and I didn’t need “to ask time” for stuff or get a nod of approval from Product or Leadership. I went from two projects to five, then ten. Dispatch work and move on to the next one.</p><p>And that’s when I needed the extension and not as a toy project. Because I was switching between so many repos, so many tech stacks, so many contexts, that I genuinely needed a visual anchor to tell me where I was.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Pr1j5aOqErJZF6A0B3RdNA.png" /><figcaption>Agents work while you have time to juggle around and delegate more work</figcaption></figure><h3>The 100x Operator</h3><p>There’s a new archetype emerging. Not the 10x engineer. The <strong>100x operator</strong>.</p><p>The 100x operator isn’t the best coder in the room. They might not be a coder at all, in the traditional sense. What they are is: deeply fluent in the problem domain, architecturally opinionated, and most importantly: <em>AI-aware</em>. They know how to break a problem into components. They know how to review what the AI produces, when to trust it and when to push back. They know how to chain tools, manage context, and maintain quality without drowning in implementation.</p><p>So as Elsa says, let it go. You are no longer paid to write code. You are paid to solve problems at a much higher level.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=20cfb9103068" width="1" height="1" alt=""><hr><p><a href="https://tech.healthee.com/let-it-go-youre-not-getting-paid-to-write-code-anymore-20cfb9103068">Let It Go: You’re Not Getting Paid To Write Code Anymore</a> was originally published in <a href="https://tech.healthee.com/">Healthee</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Gentle Introduction for Becoming a Powerhouse Developer with Claude Code: Rules, Skills and…]]></title>
            <link>https://tech.healthee.com/a-gentle-introduction-for-becoming-a-powerhouse-developer-with-claude-code-rules-skills-and-13dc6a7a4dfd?source=rss-892279cf5e14------2</link>
            <guid isPermaLink="false">https://medium.com/p/13dc6a7a4dfd</guid>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[cursor]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[claude]]></category>
            <dc:creator><![CDATA[Yoav Vollansky]]></dc:creator>
            <pubDate>Mon, 26 Jan 2026 10:40:21 GMT</pubDate>
            <atom:updated>2026-01-26T11:46:37.814Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JhcZ3i9QT7R8_ywsUzwAHQ.png" /></figure><h3>Become a Powerhouse Developer with Claude Code: Rules, Skills and Agents</h3><h3>Automating the Dev Loop: Linear Tickets, GitHub Branches, and CI Monitoring with Claude Code</h3><p>Every developer knows the friction of starting new work. You have an idea, but before writing a single line of code, there’s overhead: create a ticket, format a branch name, remember which project it belongs to, push and wait for CI, check logs when it fails. These micro-tasks aren’t hard, but they accumulate into cognitive load that fragments your focus.</p><p>Claude Code can integrate directly with Linear and GitHub to handle this overhead automatically. This article walks through a production workflow covering ticket creation, branch management, CI monitoring, code review handling, and multi-repository coordination.</p><h3><strong>The Problem</strong></h3><p>You’re deep in a debugging session. You’ve finally understood the root cause: a hash calculation that differs between two services. The fix is clear in your mind. But before you can write it, you need to: open Linear, create a ticket, think of a good name, assign it to yourself, copy the ticket ID, switch to terminal, construct a branch name following your team’s convention, run git checkout, and finally… wait, what was the fix again?</p><p>This is the real cost of process overhead. It’s not the time spent on each step (seconds, really). It’s the cognitive interruption. Every context switch forces your brain to serialize its current state, handle the interruption, then deserialize back to where you were. Some of that state inevitably gets lost.</p><p>The individual steps exist for good reasons — ticketing creates accountability, branch naming enables CI automation, status tracking helps teams coordinate. The problem isn’t the process. It’s that humans are doing mechanical work that machines should handle.</p><p>When you encounter a bug or need to implement a feature, the ideal workflow is: describe the problem, then work on the problem. Everything else — ticket creation, branch management, CI monitoring, work logging — should happen automatically in the background while you stay focused on the actual engineering challenge.</p><p>That’s what this setup achieves. You tell Claude what you’re working on. Claude handles the ceremony. You stay in flow.</p><h3><strong>Part 1: Linear Integration via MCP</strong></h3><p>The Model Context Protocol (MCP) is an open standard that lets AI assistants communicate with external services through a unified interface. Linear provides an official MCP server, which means Claude Code can create tickets, update statuses, and query issues directly through conversation.</p><p>Setting it up takes one command:</p><pre>claude mcp add linear-server https://mcp.linear.app/sse — transport sse</pre><p>Once connected, Claude gains access to Linear’s full API surface — creating issues, listing them, updating statuses, managing projects. But raw API access isn’t enough. You need to teach Claude <em>*how*</em> you want to use it.</p><p><strong>Creating a Skill for Ticket Creation</strong></p><p>Skills are workflow definitions written in markdown. They tell Claude what steps to follow when a particular situation arises. Think of them as runbooks that Claude executes on your behalf.</p><p>For ticket creation, you want Claude to understand your team structure, infer the right project from context, suggest a good ticket name, and then automatically check out the corresponding branch. Here’s a simplified version of what that skill looks like:</p><blockquote>— -</blockquote><blockquote>name: new-ticket</blockquote><blockquote>description: Creates a Linear ticket before starting work on a feature or fix.</blockquote><blockquote><strong>— -</strong></blockquote><blockquote># Create New Linear Ticket</blockquote><blockquote>## Instructions</blockquote><blockquote>1. **Analyze Context**: Look at the current conversation, working directory,</blockquote><blockquote>and recent commits to understand what work is planned.</blockquote><blockquote>2. **Suggest Ticket Name**: Generate a concise, descriptive title.</blockquote><blockquote>3. **Create Ticket** using Linear MCP:</blockquote><blockquote>- Team: Data Science</blockquote><blockquote>- Assignee: me</blockquote><blockquote>- Status: In Progress</blockquote><blockquote>4. **Auto-checkout branch**: Use the `gitBranchName` field from Linear’s response:</blockquote><blockquote>```bash</blockquote><blockquote>git checkout -b &lt;gitBranchName&gt;</blockquote><blockquote>```</blockquote><p>The magic here is that Linear’s API returns a pre-formatted branch name based on the ticket. No need to manually construct `username/ticket-id-slug` patterns — it comes from the source of truth.</p><p>With this skill in place, starting new work becomes conversational:</p><blockquote><strong>User</strong>: I need to fix the hash mismatch bug in the docling processor</blockquote><blockquote><strong>Claude</strong>: Based on our discussion, I suggest:</blockquote><blockquote>**Ticket Name**: Fix docling hash mismatch between driller and predictor</blockquote><blockquote>**Project**: Driller EOC</blockquote><blockquote>**Team**: Data Science</blockquote><blockquote>Should I create this ticket?</blockquote><blockquote><strong>User</strong>: yes</blockquote><blockquote><strong>Claude</strong>: Created DAT-1517: “Fix docling hash mismatch…”</blockquote><blockquote>URL: <a href="https://linear.app/healthee/issue/DAT-1517">https://linear.app/healthee/issue/DAT-1517</a></blockquote><blockquote>Switched to branch: yoavvollansky/dat-1517-fix-docling-hash-mismatch</blockquote><p>From problem description to tracked ticket to ready-to-code branch in seconds, without leaving your editor.</p><h3><strong>Part 2: Automatic CI Monitoring</strong></h3><p>Pushing code and waiting for CI is another flow-breaker. You push, switch to GitHub, wait for the workflow to appear, refresh to see status, and if it fails, dig through logs to find the problem. This back-and-forth can happen multiple times per feature.</p><p>Claude Code can use the GitHub CLI (`gh`) to monitor CI status as part of the development loop. The key is telling Claude that CI passing is part of task completion, not a separate concern.</p><p>Add this rule to your project’s `CLAUDE.md` file:</p><blockquote>After any push to a branch, check GitHub Actions workflow status using</blockquote><blockquote>`gh run list` or `gh run view`. If tests fail, analyze the failure logs</blockquote><blockquote>and fix the tests. Verify tests pass before considering the task complete.</blockquote><p>This single rule changes Claude’s behavior significantly. After pushing, Claude doesn’t just report “pushed successfully” — it follows through to verify the work actually builds and passes tests. If something fails, Claude examines the logs and attempts to fix the issue before reporting completion.</p><p>Here’s what that looks like in practice:</p><blockquote>Claude: Pushed changes to yoavvollansky/dat-1517-fix-docling-hash-mismatch</blockquote><blockquote>Let me check CI status…</blockquote><blockquote>$ gh run list — branch yoavvollansky/dat-1517-fix-docling-hash-mismatch — limit 1</blockquote><blockquote>STATUS NAME WORKFLOW BRANCH</blockquote><blockquote>X CI ci.yml yoavvollansky/dat-1517-fix-docling-hash-mismatch</blockquote><blockquote>CI failed. Let me check the logs…</blockquote><blockquote>$ gh run view 12345678 — log-failed</blockquote><blockquote>Test failure in test_hash_calculation:</blockquote><blockquote>AssertionError: Expected hash ‘abc123’ but got ‘def456’</blockquote><blockquote>The test is using a hardcoded expected hash. I’ll update it to match the</blockquote><blockquote>new hash calculation logic…</blockquote><blockquote>[fixes test]</blockquote><blockquote>Pushed fix. Checking CI again…</blockquote><blockquote>$ gh run list — branch yoavvollansky/dat-1517-fix-docling-hash-mismatch — limit 1</blockquote><blockquote>STATUS NAME WORKFLOW BRANCH</blockquote><blockquote>✓ CI ci.yml yoavvollansky/dat-1517-fix-docling-hash-mismatch</blockquote><blockquote>CI passing. Task complete.</blockquote><p>This loop continues until CI passes or Claude encounters something that genuinely requires human judgment. The result is that “done” actually means done — code pushed, tests passing, ready for review.</p><h3><strong>Part 3: Agents vs Skills</strong></h3><p>Both skills and agents extend Claude Code’s capabilities, but they serve fundamentally different purposes. Understanding when to use each is key to building effective workflows.</p><p><strong>Skills: Context-Aware Workflows</strong></p><p>Skills run in the same conversation context as your interaction with Claude. This means a skill can reference anything discussed earlier in the conversation — it has full awareness of what you’ve been working on.</p><p>Skills are ideal for:</p><p>- User-invocable workflows like `/new-ticket` or `/deploy`</p><p>- Tasks that need to understand the current conversation</p><p>- Simple tool orchestration where the steps are predictable</p><p>The structure is straightforward — a markdown file with frontmatter and instructions:</p><blockquote>— -</blockquote><blockquote>name: my-skill</blockquote><blockquote>description: When to trigger this skill</blockquote><blockquote><strong>— -</strong></blockquote><blockquote># Instructions for Claude to follow</blockquote><blockquote>…</blockquote><blockquote>…</blockquote><p>When you invoke a skill, it’s like giving Claude a detailed runbook to follow while maintaining full awareness of your conversation history.</p><p><strong>Agents: Autonomous Subprocesses</strong></p><p>Agents are different. They spawn as separate subprocesses with their own context, tools, and execution environment. An agent might run in the background, gathering information from multiple sources, and return a single consolidated result.</p><p>Agents excel at:</p><p>- Autonomous multi-step research</p><p>- Tasks that need to query external sources (GitHub, Linear, etc.)</p><p>- Background operations that shouldn’t block your main workflow</p><p>- Work that benefits from isolation from the main conversation</p><p>The structure is similar to skills but includes additional configuration:</p><blockquote>— -</blockquote><blockquote>name: my-agent</blockquote><blockquote>description: When Claude should spawn this agent</blockquote><blockquote>model: sonnet</blockquote><blockquote>— -</blockquote><blockquote># Agent’s system prompt and instructions</blockquote><p>The key difference is that agents don’t automatically see your conversation history. They gather their own context from the sources you configure them to query.</p><p><strong>When to Use Which</strong></p><p>The decision comes down to where the context lives.</p><p><strong>Skills:</strong></p><p>- Context: Full conversation history</p><p>- Execution: Inline (same process)</p><p>- Background: No</p><p>- Tool access: All tools</p><p>- Best for: Workflows needing chat context</p><p><strong>Agents:</strong></p><p>- Context: Isolated (unless explicitly shared)</p><p>- Execution: Subprocess</p><p>- Background: Yes (can run async)</p><p>- Tool access: Defined subset per agent type</p><p>- Best for: Autonomous research/operations</p><p>A practical example: ticket creation works well as a skill because Claude needs to understand what you’ve been discussing to suggest the right ticket name. Work logging works better as an agent because it gathers context from external sources (GitHub PRs, Linear issues, git history) rather than from the conversation.</p><p><strong>Backing Rules</strong></p><p>Here’s something that’s easy to miss: agents need triggers. An agent definition sitting in `~/.claude/agents/` won’t do anything if Claude doesn’t know when to invoke it.</p><p>Every agent should have a corresponding rule in your `CLAUDE.md` that tells Claude when to spawn it:</p><blockquote><strong># Rule 28: CI Monitoring</strong></blockquote><blockquote>After any push, check GitHub Actions using `gh run list`. If tests fail,</blockquote><blockquote>analyze logs and fix. Use the github-ops agent for complex CI investigations.</blockquote><blockquote><strong># Rule 31: Work Logging</strong></blockquote><blockquote>At the end of a work session or after completing significant tasks, use the</blockquote><blockquote>daily-work-logger agent to document accomplishments across all repositories.</blockquote><p>Without these backing rules, you have agents that exist but never run. The rule defines <em>*when*</em>, the agent defines <em>*how*</em>.</p><h3><strong>Part 4: Multi-Repository Workflows</strong></h3><p>Modern software rarely lives in a single repository. A feature might touch an API service and its infrastructure configuration. A bug fix might require coordinated changes to a client library and the server that consumes it. When work spans multiple repos, the coordination overhead multiplies.</p><p><strong>The Challenge</strong></p><p>Consider a typical cross-repo change: you need to add a parameter to a service’s API (repo A) and update the client that calls it (repo B). The changes are coupled — deploying one without the other breaks things. You need two tickets, two branches, two sets of commits, and careful deployment ordering.</p><p>Traditional approach: two terminal windows, two sets of commits, manual tracking of which is deployed, and hoping you don’t forget the deployment order.</p><p><strong>Claude’s Cross-Repo Awareness</strong></p><p>By configuring agents to search across your entire GitHub organization rather than just the current directory, you can maintain visibility across all your work. The key is using GitHub’s search API rather than local git commands:</p><p>Search ALL PRs you authored across the entire org</p><pre>gh search prs — author=”@me” — created=”&gt;$(date -v-7d +%Y-%m-%d)” — json repository,title,state,url</pre><p>Get your activity across all repos</p><pre>gh api “/users/$(gh api user — jq ‘.login’)/events” — jq ‘.[] | select(.type==”PushEvent”)’</pre><p>When Claude uses these commands, it surfaces work regardless of which directory you’re currently in. A work logging agent configured this way can report:</p><blockquote>Found activity in 3 repositories today:</blockquote><blockquote>**GetHealthee/Driller** (2 PRs)</blockquote><blockquote>- #241: Add hash debug logging — MERGED</blockquote><blockquote>- #242: Pass use_cache parameter — IN REVIEW</blockquote><blockquote>**GetHealthee/infrastructure** (2 PRs)</blockquote><blockquote>- #1345: Update predictor image to v168 — MERGED</blockquote><blockquote>- #1346: Add use_cache to predictor schema — MERGED</blockquote><blockquote>**GetHealthee/api-gateway** (1 PR)</blockquote><blockquote>- #89: Add cache-control headers — IN REVIEW</blockquote><p>This visibility is valuable. You can see your full day’s work in one place, not scattered across repo-specific git logs.</p><p><strong>Coordinated Tickets</strong></p><p>Linear’s issue linking helps manage cross-repo dependencies. When you describe work that spans repositories, Claude can create linked tickets with proper dependency relationships:</p><blockquote><strong>User</strong>: I need to add a use_cache parameter — it needs changes in both the</blockquote><blockquote>infrastructure predictor and the driller client</blockquote><blockquote><strong>Claude</strong>: This spans two repos. I’ll create linked tickets:</blockquote><blockquote>Created DAT-1521: “Add use_cache parameter to predictor” (Infrastructure)</blockquote><blockquote>Created DAT-1522: “Pass use_cache to SageMaker calls” (Driller)</blockquote><blockquote>→ Linked as “blocked by DAT-1521”</blockquote><blockquote>Checking out branches…</blockquote><blockquote>In /infrastructure: user/dat-1521-add-use-cache-to-predictor</blockquote><blockquote>In /driller: user/dat-1522-pass-use-cache-to-sagemaker</blockquote><p>The “blocked by” relationship encodes the deployment order directly in your ticket system. Anyone looking at DAT-1522 can see it depends on DAT-1521 being completed first.</p><p><strong>IDE Integration</strong></p><p>IDEs like Cursor amplify multi-repo workflows by letting you work with multiple repositories in a single workspace. With several repo folders open together, you get:</p><p>- <strong>Visual git state</strong>: Branch names, uncommitted changes, and sync status for all repos at once in the Source Control panel</p><p>- <strong>Unified search</strong>: Find references across service boundaries without switching contexts</p><p>- <strong>Split editing</strong>: Modify the client call in one pane, the server handler in another, seeing both simultaneously</p><p>This visual awareness complements Claude’s API-level awareness. You handle the code changes with full visual context; Claude handles the ticket tracking, CI monitoring, and work logging across all repos simultaneously.</p><p><strong>The Deployment Pattern</strong></p><p>For coordinated changes across services, a consistent pattern emerges:</p><p>1. <strong>Create linked tickets</strong> with explicit dependencies</p><p>2. <strong>Implement server-side first</strong> (the thing being called)</p><p>3. <strong>CI passes</strong> → merge and deploy server</p><p>4. <strong>Implement client-side</strong> (the thing doing the calling)</p><p>5. <strong>CI passes</strong> → merge and deploy client</p><p>Claude can track this progression and prompt you when dependencies resolve:</p><blockquote>Claude: DAT-1521 (infrastructure) is merged and deployed.</blockquote><blockquote>DAT-1522 (driller) is now unblocked. Ready to merge?</blockquote><p><strong>Consolidated Work Logging</strong></p><p>You’ve done impressive work across multiple repositories with automated contributions. How do you keep track of it all? How do you clearly communicate progress to your manager? With so much AI-generated code flying around, even you can lose the thread. That’s where daily-work-logger comes in — an agent that ties everything together by generating a single, structured log of your work across all repositories, organized by day and project:</p><blockquote><strong># Weekly Work Log — Week 04, 2026</strong></blockquote><blockquote><strong>## Monday, January 20, 2026</strong></blockquote><blockquote><strong>### Project: Infrastructure (SageMaker)</strong></blockquote><blockquote>- 11:28 <strong>**MERGED: Added use_cache parameter**</strong> (DAT-1521)</blockquote><blockquote>- PR #1346, Files: predictor.py</blockquote><blockquote><strong>### Project: Driller</strong></blockquote><blockquote>- 11:45 <strong>**IN REVIEW: Pass use_cache to SageMaker**</strong> (DAT-1522)</blockquote><blockquote>- PR #242, Files: docling_utils.py</blockquote><blockquote>- Blocked by: DAT-1521 ✓ (now resolved)</blockquote><p>One log, multiple repos, automatic correlation via Linear ticket IDs in commit messages. At the end of a week, you have a complete record of what was accomplished without manual tracking.</p><h3>Part 5: Handling Code Reviews</h3><p>The development loop doesn’t end when CI passes. There’s still the review gauntlet: automated bots (Cursor Bot, GitHub Copilot), team leads, or — occasionally — actual humans who read your code and have opinions about it.</p><p>Review comments create another context-switching problem. You’ve moved on to the next task. A notification arrives: “3 new comments on your PR.” Now you need to context-switch back, remember what the code was doing, parse each comment, decide if it’s valid, implement fixes or craft responses, and push again. For a PR with multiple review rounds, this ping-pong can stretch across days.</p><p><strong>The Review Analysis Skill</strong></p><p>A skill for handling code reviews fits naturally here because it needs full conversation context — you might have discussed the implementation approach earlier, and that context informs whether a reviewer’s suggestion makes sense.</p><p>The `check-git-comments` skill analyzes PR feedback with a critical eye:</p><blockquote>— -</blockquote><blockquote>name: check-git-comments</blockquote><blockquote>description: Analyzes GitHub PR comments and suggests responses or fixes.</blockquote><blockquote><strong>— -</strong></blockquote><blockquote><strong># Analyze PR Review Comments</strong></blockquote><blockquote><strong>## Instructions</strong></blockquote><blockquote>1. <strong>**Fetch comments**</strong> using `gh pr view — comments` or the GitHub API</blockquote><blockquote>2. <strong>**For each comment**</strong>, analyze:</blockquote><blockquote>- Is the feedback technically valid?</blockquote><blockquote>- Does it conflict with requirements discussed earlier?</blockquote><blockquote>- Is it a style preference vs. a correctness issue?</blockquote><blockquote>3. <strong>**Categorize and respond**</strong>:</blockquote><blockquote>- Valid fix needed → suggest the code change</blockquote><blockquote>- Valid but already addressed → point to the relevant code</blockquote><blockquote>- Disagree with feedback → draft a respectful response explaining why</blockquote><blockquote>4. <strong>**Present all suggestions**</strong> for user approval before taking action</blockquote><p>The key here is <em>*critical analysis*</em>, not blind compliance. Automated review bots are useful but imperfect. They flag potential issues without understanding intent. A good review handler distinguishes between:</p><p>- <strong>Actionable feedback</strong>: “This function doesn’t handle the empty array case” → implement the fix</p><p>- <strong>Style preferences</strong>: “Consider using `forEach` instead of `for`” → acknowledge or push back based on team conventions</p><p>- <strong>False positives</strong>: “Unused variable `config`” when it’s actually used in a dynamic import → explain why the bot is wrong</p><p><strong>The Workflow in Practice</strong></p><p>After pushing a PR, the review cycle might look like this:</p><blockquote>[Cursor Bot leaves 4 comments on PR #242]</blockquote><blockquote><strong>User</strong>: Check the comments on my PR</blockquote><blockquote><strong>Claude</strong>: Analyzing comments on PR #242…</blockquote><blockquote>**Comment 1** (line 45): “Consider adding error handling for null response”</blockquote><blockquote>→ Valid. The SageMaker endpoint can return null on timeout. I’ll add a check.</blockquote><blockquote>**Comment 2** (line 67): “Magic number 30 should be a constant”</blockquote><blockquote>→ Valid style improvement. I’ll extract to CACHE_TTL_SECONDS.</blockquote><blockquote>**Comment 3** (line 89): “Unused import: hashlib”</blockquote><blockquote>→ False positive. hashlib is used in compute_hash() on line 142. I’ll reply</blockquote><blockquote>explaining this.</blockquote><blockquote>**Comment 4** (line 112): “Function is too long (&gt;50 lines)”</blockquote><blockquote>→ Disagree. This function handles a complete workflow and splitting it would</blockquote><blockquote>obscure the logic. Recommend responding with rationale.</blockquote><blockquote>Proposed actions:</blockquote><blockquote>1. Fix null response handling (Comment 1)</blockquote><blockquote>2. Extract magic number to constant (Comment 2)</blockquote><blockquote>3. Reply to Comment 3: “hashlib is used on line 142 in compute_hash()”</blockquote><blockquote>4. Reply to Comment 4: “Keeping as single function for readability — the steps</blockquote><blockquote>are sequential and tightly coupled”</blockquote><blockquote>Proceed with these changes and responses?</blockquote><blockquote><strong>User</strong>: yes</blockquote><blockquote><strong>Claude</strong>: [implements fixes, posts responses, pushes]</blockquote><blockquote>Changes pushed. Responded to comments 3 and 4.</blockquote><p>The developer stays focused. They review Claude’s analysis, approve or adjust, and move on. No need to mentally re-enter the code context just to handle review feedback.</p><p><strong>Human Reviewers</strong></p><p>The same skill works for human reviewers, but the tone shifts. Bot feedback tends to be mechanical and can be addressed efficiently. Human feedback often carries implicit context — a senior engineer’s “consider whether this scales” might be a genuine question or a polite way of saying “this doesn’t scale, fix it.”</p><p>Claude can parse the comment, but the judgment call remains with you:</p><blockquote>**Comment from @tech-lead** (line 200): “Have we considered the memory</blockquote><blockquote>implications of caching these objects?”</blockquote><blockquote>Analysis: This appears to be a architectural concern rather than a specific</blockquote><blockquote>bug. The current implementation caches parsed documents in memory without</blockquote><blockquote>eviction policy.</blockquote><blockquote>Options:</blockquote><blockquote>A) Add LRU eviction with configurable max size</blockquote><blockquote>B) Move to Redis for distributed caching</blockquote><blockquote>C) Respond asking for clarification on expected scale</blockquote><blockquote>Recommendation: Start with © to understand requirements before implementing.</blockquote><blockquote>Draft response: “Good question — currently scoped for single-instance deployment</blockquote><blockquote>with ~100 concurrent documents. If we need to scale beyond that, I’d suggest</blockquote><blockquote>moving to Redis. What’s the expected load for this service?”</blockquote><p>You review, adjust the tone if needed, and approve. The skill handles the mechanical parts — fetching comments, correlating them to code, drafting responses — while you make the judgment calls.</p><p><strong>Why a Skill, Not an Agent</strong></p><p>Review handling works better as a skill than an agent because:</p><p>1. <strong>Conversation context matters</strong>: You might have discussed why you chose a particular approach. That context helps evaluate whether a reviewer’s alternative suggestion makes sense.</p><p>2. <strong>Approval is required</strong>: You don’t want an agent autonomously responding to your tech lead’s comments. Every response and code change needs human sign-off.</p><p>3. <strong>It’s interactive</strong>: The back-and-forth (“what about comment 3?” / “actually, let’s push back on that one”) benefits from the conversational flow.</p><p>Agents work well for autonomous background tasks. Review handling is inherently collaborative — Claude analyzes and suggests, you decide and approve.</p><p><strong>Setup Checklist</strong></p><p>Getting this workflow running requires a few one-time setup steps:</p><p><strong>1. Linear MCP</strong></p><pre>claude mcp add linear-server https://mcp.linear.app/sse — transport sse</pre><p><strong>2. GitHub CLI</strong> (authenticate if not already)</p><pre>gh auth login</pre><p><strong>3. Skills directory</strong> (for user-invocable workflows)</p><pre>mkdir -p ~/.claude/skills/new-ticket</pre><pre>mkdir -p ~/.claude/skills/check-git-comments</pre><p>Then Create SKILL.md files with your workflows</p><p><strong>4. Agents directory</strong> (for autonomous operations)</p><pre>mkdir -p ~/.claude/agents</pre><p>Create agent .md files</p><p><strong>5. Project rules</strong> (`CLAUDE.md` in your repo root)</p><blockquote><strong># CI Monitoring</strong></blockquote><blockquote>After any push, check GitHub Actions…</blockquote><blockquote><strong># Ticket Creation</strong></blockquote><blockquote>Before starting work on main/DEV branch, create a Linear ticket…</blockquote><p>etc.</p><p><strong>The Result</strong></p><p>With these pieces in place, the development loop compresses significantly. Starting new work no longer requires leaving your editor or manually coordinating across tools:</p><blockquote><strong>User</strong>: I need to add caching to the API</blockquote><blockquote><strong>Claude</strong>: [creates Linear ticket via MCP]</blockquote><blockquote>Created DAT-1520: “Add caching to API”</blockquote><blockquote>Switched to branch: user/dat-1520-add-caching-to-api</blockquote><blockquote>[writes code]</blockquote><blockquote>Pushed. Checking CI…</blockquote><blockquote>CI failed: test_cache_invalidation</blockquote><blockquote>Fixing…</blockquote><blockquote>[fixes test, pushes]</blockquote><blockquote>CI passing. DAT-1520 ready for review.</blockquote><p>The mechanical overhead disappears. Tickets get created, branches get named correctly, CI gets monitored, work gets logged — all automatically, while you focus on the actual problem you’re solving.</p><p>That’s the goal: not to remove process, but to automate it so thoroughly that proper hygiene becomes the path of least resistance.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=13dc6a7a4dfd" width="1" height="1" alt=""><hr><p><a href="https://tech.healthee.com/a-gentle-introduction-for-becoming-a-powerhouse-developer-with-claude-code-rules-skills-and-13dc6a7a4dfd">A Gentle Introduction for Becoming a Powerhouse Developer with Claude Code: Rules, Skills and…</a> was originally published in <a href="https://tech.healthee.com/">Healthee</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to Generate Useful Project Management Insights With AI and LLMs (While You Sleep)]]></title>
            <link>https://tech.healthee.com/how-to-generate-useful-project-management-insights-with-ai-and-llms-while-you-sleep-a7b6bd794bf3?source=rss-892279cf5e14------2</link>
            <guid isPermaLink="false">https://medium.com/p/a7b6bd794bf3</guid>
            <category><![CDATA[openai]]></category>
            <category><![CDATA[slack]]></category>
            <category><![CDATA[automation]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[project-management]]></category>
            <dc:creator><![CDATA[Yoav Vollansky]]></dc:creator>
            <pubDate>Wed, 24 Jul 2024 13:36:19 GMT</pubDate>
            <atom:updated>2024-07-26T11:58:33.158Z</atom:updated>
            <content:encoded><![CDATA[<h4>Or How to Impress Your Manager by Reporting Your Daily In-Progress Tasks Like a Boss 😎</h4><p>Envision a project management system that integrates data from tools like Linear, Slack, OpsGenie, Sentry, HubSpot, Jira, AWS CloudTrail, Prometheus, and even your code on GitHub. By leveraging LLMs such as OpenAI’s GPT-4 or Lllama-3 you can analyze and synthesize information from these diverse sources to provide real-time, actionable updates — and even suggest solutions — based on this data. This kind of sophisticated integration enhances decision-making and streamlines workflows. As an example, let’s explore a tiny “tip of the iceberg” example and see how to automate daily or weekly task reporting, making this process effortless and effective.</p><p>At Healthee, we love using Linear. It’s flexible, it’s versatile, and it makes project management as smooth as a well-oiled machine. If you manage projects, I can confidently assure you that Linear is a game-changer since it keeps tasks organized, deadlines manageable, insights clear and our sanity intact.</p><p>But one thing that we enjoy even more than efficient project management is <strong>automating stuff</strong>. Imagine you walk into the office (or roll out of bed if you’re working from home), and your manager has already seen a beautifully summarized update of your daily in-progress tasks in Slack. With the joined forces of Linear, OpenAI, and Slack, you can automate this process and keep your manager informed effortlessly. Here’s how to do it.</p><h3>Step 1: Setting Up Linear</h3><p>First, you’ll need a Linear account and an API key. If you’re not working with Linear yet, now’s a good time to start. Generate an API Key by navigating to your settings in Linear and creating it.</p><h3>Step 2: Fetching Tasks from Linear</h3><p>We will write a Python script to fetch your in-progress tasks from Linear. You can choose which kinds of tasks to include — just in-progress, in review, or others based on your preference. You can get Linear data using a GraphQL query. Here’s the code:</p><pre>import requests<br><br>LINEAR_API_KEY = &#39;your_linear_api_key&#39;<br>LINEAR_API_URL = &#39;https://api.linear.app/graphql&#39;<br>ALLOWED_STATE_NAMES = {&quot;Todo&quot;, &quot;In Progress&quot;, &quot;Backlog&quot;, &quot;Done&quot;}<br><br>def fetch_linear_user_id():<br>    query = &quot;&quot;&quot;<br>    query {<br>      viewer {<br>        id<br>      }<br>    }<br>    &quot;&quot;&quot;<br>    headers = {<br>        &#39;Authorization&#39;: LINEAR_API_KEY,<br>        &#39;Content-Type&#39;: &#39;application/json&#39;<br>    }<br>    response = requests.post(LINEAR_API_URL, json={&#39;query&#39;: query}, headers=headers)<br>    if response.status_code == 200:<br>        return response.json()[&#39;data&#39;][&#39;viewer&#39;][&#39;id&#39;]<br>    else:<br>        raise Exception(f&quot;Failed to fetch user ID: {response.status_code} {response.text}&quot;)<br><br>def fetch_tasks(user_id, state_names=[&quot;Todo&quot;, &quot;In Progress&quot;]):<br>    invalid_state_names = [state for state in state_names if state not in ALLOWED_STATE_NAMES]<br>    if invalid_state_names:<br>        raise ValueError(<br>            f&quot;Invalid state names: {&#39;, &#39;.join(invalid_state_names)}. Allowed state names are: {&#39;, &#39;.join(ALLOWED_STATE_NAMES)}&quot;)<br><br>    query = &quot;&quot;&quot;<br>    query($assigneeId: ID!, $stateNames: [String!]!) {<br>      issues(filter: {state: {name: {in: $stateNames}}, assignee: {id: {eq: $assigneeId}}}) {<br>        nodes {<br>          title<br>          description<br>          state {<br>            name<br>          }<br>          labels {<br>            nodes {<br>              name<br>            }<br>          }<br>          projectMilestone {<br>            name<br>          }<br>          project {<br>            name<br>          }<br>          comments {<br>            nodes {<br>              body<br>            }<br>          }<br>        }<br>      }<br>    }<br>    &quot;&quot;&quot;<br>    variables = {&quot;assigneeId&quot;: user_id, &quot;stateNames&quot;: state_names}<br>    headers = {<br>        &#39;Authorization&#39;: LINEAR_API_KEY,<br>        &#39;Content-Type&#39;: &#39;application/json&#39;<br>    }<br>    response = requests.post(LINEAR_API_URL, json={&#39;query&#39;: query, &#39;variables&#39;: variables}, headers=headers)<br>    if response.status_code == 200:<br>        return response.json()[&#39;data&#39;][&#39;issues&#39;][&#39;nodes&#39;]<br>    else:<br>        raise Exception(f&quot;Failed to fetch tasks: {response.status_code} {response.text}&quot;)<br><br>user_id = fetch_linear_user_id()<br>tasks = fetch_tasks(user_id, [&quot;Todo&quot;, &quot;In Progress&quot;, &quot;In Review&quot;])</pre><h3>Step 3: Getting Insights from Tasks with OpenAI</h3><p>Now comes the exciting part: harnessing the power of Large Language Models (LLMs) like OpenAI’s GPT-4 to summarize our tasks. LLMs can take your verbose, detailed task descriptions and distill them into concise, easy-to-read summaries. It can tell you which projects they belong to, how urgent they are, and what labels are attached to them if that’s what you’d like. It’s like having your very own J.A.R.V.I.S. from the Avengers, minus the British accent.</p><p>To get started, we’ll send the task details to OpenAI and receive a succinct summary in return. Here’s how you can set up the OpenAI API call:</p><pre>from openai import OpenAI<br><br>openai.api_key = &#39;your_openai_api_key&#39;<br><br>def summarize_tasks(tasks):<br>    client = penAI(api_key=OPENAI_API_KEY)<br>    prompt = PROMPT + tasks<br><br>    response = client.chat.completions.create(<br>        messages=[<br>            {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: prompt}<br>        ],<br>        model=&quot;gpt-4o&quot;,<br>        temperature=0.7<br>    )<br><br>    summary = response.choices[0].message.content<br>    return summary</pre><h4>Crafting Effective Prompts</h4><p>When using OpenAI, the quality of your results largely depends on how well you craft your prompts. This is known as prompt engineering, and it’s an essential skill to get the most out of AI. A well-crafted prompt can guide the AI to produce precise, relevant, and high-quality responses, while a poorly constructed prompt can lead to vague or off-target results.</p><h4>Tips for Effective Prompt Engineering</h4><ol><li>Be Specific: The more specific your prompt, the better the AI can understand and respond to your needs. Specify what you want the summary to focus on, such as key actions, deadlines, or pending items.</li><li>Provide Context: Giving the AI some context can help it generate more accurate summaries. For example, if you’re summarizing tasks for a project, mention the project name and any relevant details.</li><li>Use Examples: Including examples in your prompt can show the AI exactly what kind of response you’re looking for. This can be especially helpful for complex tasks or detailed summaries.</li><li>Iterate and Refine: Prompt engineering is often an iterative process. You may need to refine your prompts based on the AI’s initial responses to get the desired output.</li></ol><p>I know, you must be thinking “why isn’t he just giving us the bloody prompt?” (with a British accent of course). well, here it is:</p><pre>PROMPT = &quot;&quot;&quot; Im going to tell you my tasks for today as coming from the Linear app API. Please summarize them into bullet points in the following way:<br>    - Tasks that are &quot;In Progress&quot; should get a bullet point each. Say what they are, which project they belong to, and what the milestone is. Also, mention any labels and comments if exist very briefly. For example, if the task is &quot;Fix the bug&quot;, the project is &quot;Driller&quot;, the milestone is &quot;v1.0&quot;, and the label is &quot;Critical&quot;, you should write:<br>    - Fix the bug in Driller project, v1.0 milestone, Critical&quot;. You won&#39;t mentioned any comments, because there are none. <br>    - All tasks that are &quot;Todo&quot; should be summarized in a single bullet point and mentioned briefly. <br>    - Don&#39;t be very formal, it is a slack message. <br>    - When mentioning a milestone, just mention it in brackets. For example, &quot;Driller project (scoping)&quot;. Where &quot;scoping&quot; is the milestone. No need to mention the word &quot;milestone&quot;.<br>    - if two tasks are in the same project and milestone, you can mention them in the same bullet point.<br>    - don&#39;t explicitly mention a label. If the label is &quot;Improvement&quot;, you can start the bullet point like &quot;Improving... this and that&quot;. If the label is &quot;Feature&quot; you can start the bullet point like &quot;Adding a feature...&quot;. If the label is &quot;bug&quot;, you can start the bullet point like &quot;Fixing a bug...&quot;.<br>    - arrange groups of tasks in the same project and milestone together (as in following bullet points). <br>    <br>    - IT IS VERY IMPORTANT to only use present tense (as in, &quot;doing&quot;, &quot;fixing&quot; etc) to tasks that are &quot;In Progress&quot; only!<br>    - Tasks that are &quot;Todo&quot; should be mentioned all in one bullet point (the last one) starting with &quot;Pending Todo tasks:...&quot; and then mentioning the tasks in a comma separated list. Just mention the task without anything else (no project, no milestone, no labels, no comments).<br>    - If there are &quot;In Review&quot; tasks, you must mention them in a single bullet point at the end, starting with &quot;Tasks currently in review:...&quot; and then mentioning the tasks in a comma separated list. Just mention the task without anything else (no project, no milestone, no labels, no comments).<br>    - Symmaries comments if there are any. Don&#39;t just write them but make sense of them as a whole and summarize them.<br>    <br>    - The bullet point should be the round one.<br><br>These are my tasks in JSON format: </pre><h3>Step 4: Configuring a Slack App</h3><p>Before we can post messages to Slack, you need to configure a Slack app and get a bot token. Here’s how you can do it:</p><ol><li>Create a Slack App: Go to the Slack API and create a new app.</li><li>Add OAuth Scopes: Under “OAuth &amp; Permissions”, add the following scopes: chat:write, channels:history.</li><li>Install the App: Install the app to your workspace to get a bot token.</li><li>Get the Bot Token: Copy the bot token, which you’ll use to authenticate API requests.</li><li>Make sure you get the channel ID from slack (search it somewhere in channel settings</li><li>If you’re of the jester kind, search for your manager’s user ID as well.</li></ol><p>Now, here’s where the fun comes in. You can really confuse people by naming the Slack app after yourself and using the same profile picture as your avatar. Imagine the bewilderment on everyone’s faces when they see “you” posting updates at 6:30 AM. You digital doppelgänger will be working while you still turn in your bed.</p><p>But of course, I would never trick my boss like that. Ever. Really.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rJzgI211dZd4EUyiUMxnqA.png" /><figcaption>“While you were sleeping”. Dalle-3</figcaption></figure><h3>Step 5: Slack SDK</h3><pre>from slack_sdk import WebClient<br>from slack_sdk.errors import SlackApiError<br><br>SLACK_BOT_TOKEN = &#39;your_slack_bot_token&#39;<br>CHANNEL_ID = &#39;your_channel_id&#39;<br>BOSS_USER_ID = &#39;your_boss_user_id&#39;<br><br>client = WebClient(token=SLACK_BOT_TOKEN)<br><br>def fetch_boss_message_ts(channel_id, boss_user_id, days_back=1):<br>    try:<br>        response = client.conversations_history(<br>            channel=channel_id,<br>            limit=100<br>        )<br>        messages = response[&#39;messages&#39;]<br>        for message in messages:<br>            if message.get(&#39;user&#39;) == boss_user_id and &quot;What did everybody do today?&quot; in message.get(&#39;text&#39;, &#39;&#39;):<br>                return message[&#39;ts&#39;]<br>    except SlackApiError as e:<br>        print(f&quot;Error fetching messages: {e.response[&#39;error&#39;]}&quot;)<br>        return None<br><br>def markdown_to_block_kit(markdown_text):<br>    block_kit = [<br>        {<br>            &quot;type&quot;: &quot;section&quot;,<br>            &quot;text&quot;: {<br>                &quot;type&quot;: &quot;mrkdwn&quot;,<br>                &quot;text&quot;: markdown_text.strip()<br>            }<br>        }<br>    ]<br>    return block_kit<br><br>def post_update_to_thread(markdown_text, channel_id, thread_ts):<br>    blocks = markdown_to_block_kit(markdown_text)<br>    try:<br>        response = client.chat_postMessage(<br>            channel=channel_id,<br>            text=markdown_text,  # Fallback text for notifications<br>            blocks=blocks,<br>            thread_ts=thread_ts<br>        )<br>        print(&quot;Message posted successfully.&quot;)<br>    except SlackApiError as e:<br>        print(f&quot;Error posting message: {e.response[&#39;error&#39;]}&quot;)<br>        return None<br>    return response<br><br># Example usage<br>boss_message_ts = fetch_boss_message_ts(CHANNEL_ID, BOSS_USER_ID)<br>if boss_message_ts:<br>    markdown_text = f&quot;&quot;&quot;<br>    *Daily Task Summary*<br><br>    {task_summary}<br>    &quot;&quot;&quot;<br>    post_update_to_thread(markdown_text, CHANNEL_ID, boss_message_ts)</pre><p>Here you’re not only posting an update in the channel but also posting an answer to a specific thread where you were asked about their daily tasks. (Yes, I know the question string is quite specific, but not if your manager is using automation of their own to ask the team about their daily updates!) When AI takes over we’re going to be nothing but faded automated replicas of one another conversing with each other, so let’s start now.</p><h4>Take it Further</h4><p>This capillary of a pipeline can be enhanced in so many ways. For example, you can ask another LLM agent to further analyze the fetched tickets and suggest ways to tackle them. You can check other Linear boards and see if your task is related to other tasks that took place in other so you know who to talk to or look at other Slack channels to see if a related topic was mentioned recently. You can analyze past issues or logs that are related to your ticket with a slightly more enhanced architecture (such as RAG or chunking). You can even simply summarize important discussions that took place in your channel last week (or month) and remind everyone what was discussed.</p><h3>The Last Step</h3><p><em>(which you’ll do on your own)</em></p><p>Finally, you would want to either run this code on a server with cron or take everything to the next level and have your bot detecting activity on Slack and respond accordingly (not that difficult to implement but an overkill for this example).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lupamcbcMNCh0hoBcA6aIg.png" /><figcaption>Let the machines do the talking. Dalle-3</figcaption></figure><h3>Jokes aside</h3><p>At Healthee, we believe that serious builders need a reliable set of tools. We go by this approach to create custom integrations and solutions that streamline our workflows and enhance our productivity. Naturally, we do this fundamentally with traditional tools and practices for analytics and monitoring (for things like infra, CI/CD, BI etc.), but when you throw AI and LLMs into the pot you you really can craft your own “homemade” solutions. This kind of automation not only keep us informed but also empowers us to make data-driven decisions swiftly.</p><p>While the idea of effortlessly informing your manager with automated Slack updates is amusing, the real potential of this setup lies in its ability to keep teams consistently informed and create sophisticated monitoring and alerting systems. By combining various data with event-based sources while leveraging the reasoning capabilities of an LLM like OpenAI’s GPT-4, teams can stay up-to-date and responsive to crucial developments.</p><p>So what’s next? Imagine integrating data from common project management tools like Linear together with other developer tools, your code base as well as analytical data all into an LLM-powered system. This setup can automatically generate personalized updates and alerts in real-time for you to act upon. But not just that: if a critical bug is reported in a project milestone, the system can instantly notify relevant team members, summarize the issue, and suggest potential fixes or actions based on historical data, best practices, and internet searches. All about that and other kinds of magic in a future article ⭐️</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a7b6bd794bf3" width="1" height="1" alt=""><hr><p><a href="https://tech.healthee.com/how-to-generate-useful-project-management-insights-with-ai-and-llms-while-you-sleep-a7b6bd794bf3">How to Generate Useful Project Management Insights With AI and LLMs (While You Sleep)</a> was originally published in <a href="https://tech.healthee.com/">Healthee</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Enhance LLM Evaluation Pipelines using PyDantic]]></title>
            <link>https://tech.healthee.com/enhance-llm-evaluation-pipelines-using-pydantic-159e92b50d50?source=rss-892279cf5e14------2</link>
            <guid isPermaLink="false">https://medium.com/p/159e92b50d50</guid>
            <category><![CDATA[pydantic]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[data-science]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Yoav Vollansky]]></dc:creator>
            <pubDate>Thu, 25 Apr 2024 17:54:54 GMT</pubDate>
            <atom:updated>2024-04-26T17:23:40.130Z</atom:updated>
            <content:encoded><![CDATA[<p>I will never forget the sense of awe that gripped me the first time I used ChatGPT. I tasked it with writing a poem about healthcare insurance to amuse my coworkers, requiring the use of domain-specific terminology while ensuring everything rhymed perfectly. It was as if the poem came straight out of a Rap musical. Call me a Hamilton fan.</p><blockquote>In the realm of co-pays, deductibles, and plans,<br>We navigate the maze with our healthcare demands.<br>“PPO, HMO,” we rap, understanding the flow,<br>Choosing our coverage, as we grow.</blockquote><blockquote>“Pre-existing conditions?” No longer a scare,<br>Reforms have made healthcare more fair.<br>With HSAs and FSAs, saving the day,<br>For our medical bills, we’ve found a way.</blockquote><blockquote>Annual check-ups, preventive is key,<br>Vaccines, screenings, all copay-free.<br>Wellness visits, keeping us prime,<br>Insurance covers, every time.</blockquote><blockquote>MRI, X-rays, the imaging dance,<br>Approval needed, it’s not left to chance.<br>High-tech scans, in the diagnostic quest,<br>Ensuring our treatment, is nothing but the best.</blockquote><blockquote>In-network, out, the terms we know,<br>EPOs and POS plans, in the insurance show.<br>Caps on out-of-pocket, a protective measure,<br>In the rap of healthcare, it’s all our treasure.</blockquote><p>Who knew SEO optimization would one day serve as a perfect excuse to publish this poem.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*acYqTUSVNnEWad4EJ85XYg.jpeg" /><figcaption><em>Image of an insurance broker moonlighting as a rapper (dalle 3)</em></figcaption></figure><p>This post explores how leveraging Pydantic for data extraction from any source significantly streamlines the evaluation process, ensuring data scheme alignment between predictions and ground truth data.</p><h3>Motivation</h3><p>It’s well-known that the landscape of Language Learning Models (LLMs) is rapidly evolving, capable of executing many non-trivial tasks and doing the heavy lifting with just a prompt in English. While creating a silly song is fun, the importance of assessing an output’s accuracy becomes critical when it involves complex data extraction and processing.</p><p>The rationale for evaluating an LLM’s output is straightforward. LLMs are not yet in a hallucination-free stage of development. Data-driven processes relying on the output of your LLM pipeline are likely to produce unreliable outputs if you don’t methodically measure accuracy across a sizable dataset.</p><h3>Evaluation Pipeline</h3><p>As data scientists, our approach to an LLM problem wouldn’t differ much from how we assess any ML model’s output. With quality prompts, evaluating the model becomes almost the sole necessity (well, that plus a considerable amount of MLOps around building robust evaluation and inference pipelines). If you are well-funded or prefer not to deal with the hassle of open-source models, you might opt for an off-the-shelf LLM like OpenAI’s GPT-4 or its less proficient alternatives. In such cases, the model comes ready-made, and the main evaluation focuses on the hyperparameters, which, in an LLM context, essentially means the prompts you crafted. This scenario also offers a free evaluation of how well-engineered your pipeline is, since your inference pipeline will likely be similar, if not simpler, than the one running the evaluation.</p><p>Implementing an evaluation pipeline in the classical sense also involves developing metrics that measure your “model’s” success. But before delving into metrics, how can you tame the beast that is an LLM to output data in your desired format? And how can you ensure your ground truth data is as streamlined as possible into your evaluation pipeline? (Assuming you have ground truth data, right? Otherwise, you can stop reading this post right now as you’re entering the “finger in the wind” realm, not data science. The outcomes can be unpredictably erroneous for you and your LLM.)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/791/1*Swy4DMOfNu_FxS_XQMEoNg.png" /><figcaption>The fastest way to disaster: rely on your LLM to perform data tasks without evaluating it (dalle 3)</figcaption></figure><h3>Pydantic: The Cornerstone for Data Scheme Consistency</h3><p>Suppose you’re dealing with heaps of PDFs that have undergone text and table extraction. Now, you wish to extract meaningful data from them in a structured manner. Here, Pydantic, a Python data validation and settings management library, plays a crucial role. Its ability to enforce type hints at runtime and provide user-friendly errors offers an incredible shortcut for both validating schema and streamlining OpenAI’s LLM evaluation pipelines.</p><p>Pydantic’s core feature — enforcing type hints at runtime — ensures data adherence to a predefined schema. This feature is invaluable for integrating with LLMs for several reasons:</p><ul><li>Function Input Validation: By using a Pydantic model as a function argument for an LLM, you ensure that the output data strictly follows the specified schema. This preemptive measure eliminates common sources of errors caused by schema mismatches, thereby enhancing your entire pipeline’s reliability.</li><li>Scheme Alignment for Predictions and Ground Truth Data: Utilizing the same Pydantic model for extracting and validating ground truth data ensures that both your “true” and “predicted” data match. This alignment is crucial for accurate evaluation, facilitating like-for-like comparisons without additional data wrangling.</li></ul><h3>Data Consolidation</h3><p>It’s essential to allow some flexibility in your schema to accommodate common scenarios where ground truth data might not be clean or in the expected format. For instance, if your focus is health insurance and your data strangely models patient coverage facts as strings, whereas the LLM retrieves them as booleans (as you rightly designed), you’d either have to include these string permutations in your prompts (a risky workaround) or allow the LLM to do its job, performing post-processing on your ground truth data before scoring. To accommodate both booleans and strings, your schema needs flexibility.</p><p>Also, consider that your inferred data might include elements of interest not present in the ground truth data. In such cases, remember to exclude these elements from the evaluation process. This means your process should retain those attributes, but your metrics should ignore them.</p><h3>In-Scheme LLM Instructions</h3><p>An important part of this entire mechanism is to utilize the way PyDantic is built to give implicit instructions (in a sense) to your LLM via <a href="https://platform.openai.com/docs/guides/function-calling">functions</a>. The descriptions and docstrings you provide for the scheme you are working with will tell the LLM when to utilize your scheme to return a structured JSON answer. For example, in our specific healthcare domain example where I extract benefit data from an SBC plan, the main scheme would look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ely0fXIoc4J8AL5q6A1Mfw.png" /></figure><p>The main scheme uses two other schemes itself, which look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bjnfGCrdIMpsdqibnhCs8A.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KK5DG8VdB4O18OqA2uQzSA.png" /></figure><p>As you can see, both the docstring and the description of each field tell the LLM how to associate the data it extracts (as instructed in the prompts) with the data structure it should output eventually.</p><p>Additionally, it is important to design your PyDantic scheme such that it would be flexible to validate both your true data and the data you get from the LLM.</p><p>Having inference in mind, you would sometimes like to include fields in your scheme that you would like to extract, but that are not in your true data. In that case, you will have no choice but to implement filtering to the “true” and “pred” data so you can compare the exact same structures.</p><p>In other words, your evaluation should be accurate which might make it “narrower” than the data you eventually decide to extract. Is it cheating? Will the evaluation score represent reality? That Depends on what you ask from your LLM (how much inference data is different than the evaluated data), how much the evaluation score looks promising, and how much human-in-the-loop you expect to include in your inference.</p><h3>Metrics</h3><p>Evaluating LLM outputs against ground truth data involves a nuanced comparison of predicted and actual values, taking into account the specific aspects of interest. Like any ML evaluation, choosing your metrics wisely is paramount.</p><p>One approach could be a “DictWise” comparison of the schema, checking the values of your true schema against your predictions. You might allow for some leniency by enabling strings in the schema to be measured with their sub-metric (such as fuzzy matching or some distance algorithms). Alternatively, developing a method to vectorize your schema — considering normalization and possibly using an embedding model for strings — could offer another solution. In this case, you should consider utilizing embeddings in order to vectorize the textual data.</p><p>Here’s a straightforward implementation of the DictWise metric:</p><pre>def compare_dicts(<br>        dict1: Dict, dict2: Dict, compare_mode: str = &quot;loose&quot;, schema=None, return_individual_scores=False<br>) -&gt; tuple[float | int, dict[Any, float | int]] | float | int:<br>    &quot;&quot;&quot;<br>    Compares two dictionaries based on a specified mode and schema, returning both the total score and individual key scores.<br>    Compare mode can be &quot;loose&quot; or &quot;strict&quot;: strict mode requires all keys and values to match, while loose mode allows<br>    for some differences in values and missing keys and is calculating the score by Levenshtein distance.<br>    LIMITATION: this metric does not penalize for extra keys in the dictionary (false positives)<br>    &quot;&quot;&quot;<br>    if schema:<br>        keys_to_compare = {field for field in schema.__fields__}<br>    else:<br>        keys_to_compare = set(dict1.keys()) | set(dict2.keys())<br><br>    total_keys = len(keys_to_compare)<br>    total_score = 0.0<br>    individual_scores = {}<br><br>    for key in keys_to_compare:<br>        in_dict1, in_dict2 = key in dict1, key in dict2<br>        if not in_dict1 or not in_dict2:<br>            individual_scores[key] = 0.0<br>            continue<br><br>        val1, val2 = dict1.get(key), dict2.get(key)<br>        if compare_mode == &quot;loose&quot;:<br>            if isinstance(val1, str) and isinstance(val2, str):<br>                distance = Levenshtein.distance(val1, val2)<br>                max_len = max(len(val1), len(val2))<br>                score = 1 - (distance / max_len) if max_len &gt; 0 else 1<br>            else:<br>                score = float(val1 == val2)<br>        elif compare_mode == &quot;strict&quot;:<br>            score = float(val1 == val2)<br><br>        total_score += score<br>        individual_scores[key] = score<br><br>    normalized_total_score = total_score / total_keys if total_keys &gt; 0 else 0<br>    if return_individual_scores:<br>        return normalized_total_score, individual_scores<br>    return normalized_total_score</pre><h3>Scoring</h3><p>I will not get much into scoring in this post, but your approach to this will heavily depend on your business logic and use case. For example, you might assign different weights to various items depends on their significance or to formulate some non-linear calculation method. Combining several metrics to produce a comprehensive score is also an option. You can also just leave it vanila.</p><h3>Practical Implementation</h3><p>To effectively implement this approach, follow these steps:</p><ol><li>Define Your Pydantic Model: Clearly outline the data schema of interest, covering all relevant fields and types. This model serves as your blueprint for data extraction and LLM input/output processing.</li><li>Data Extraction: Utilize the Pydantic model for extracting ground truth data from your sources, ensuring data validation and alignment with your schema.</li><li>LLM Integration: Maintain schema consistency by passing inputs through the Pydantic model when interfacing with the LLM. This ensures direct comparability between the LLM’s outputs and your ground truth data.</li><li>Evaluation Metrics: Design your evaluation metrics around the schema defined by your Pydantic model, ensuring relevance and alignment with the data’s characteristics.</li></ol><h3>Takeaways</h3><p>Integrating Pydantic into LLM evaluation pipelines provides a robust framework for ensuring data consistency and reliability. By employing Pydantic models for data extraction and processing, researchers and developers can ground their evaluations in a solid, error-resistant foundation. This methodology not only streamlines the evaluation process but also improves the accuracy and relevance of outcomes, advancing the development of more effective and dependable LLMs.</p><p>More about metrics in depth and a detailed explanation of an evaluation pipeline in following posts!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=159e92b50d50" width="1" height="1" alt=""><hr><p><a href="https://tech.healthee.com/enhance-llm-evaluation-pipelines-using-pydantic-159e92b50d50">Enhance LLM Evaluation Pipelines using PyDantic</a> was originally published in <a href="https://tech.healthee.com/">Healthee</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[RaDiCuGLoss!]]></title>
            <link>https://tech.healthee.com/radicugloss-cee35c04ddb1?source=rss-892279cf5e14------2</link>
            <guid isPermaLink="false">https://medium.com/p/cee35c04ddb1</guid>
            <category><![CDATA[testing]]></category>
            <category><![CDATA[search]]></category>
            <category><![CDATA[algorithms]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[search-engine-optimizati]]></category>
            <dc:creator><![CDATA[Yoav Vollansky]]></dc:creator>
            <pubDate>Thu, 27 Jul 2023 10:19:19 GMT</pubDate>
            <atom:updated>2026-01-26T10:44:44.353Z</atom:updated>
            <content:encoded><![CDATA[<h3>RaDiCuGLoss! — An NDCG Revamp</h3><h3>Let your search algorithm rise through the ranks 🪄</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*el5KxX6EAzNga2SI" /><figcaption>RaDiCuGLoss: Ranked Discounted Cumulative Gain and Loss (Image: Midjourney)</figcaption></figure><p>There are countless metrics that can be employed to gauge the precision of a search engine. Some notable examples include the commonly used <a href="https://towardsdatascience.com/accuracy-precision-recall-or-f1-331fb37c5cb9">precision, recall, and F1 scores</a>, along with more situation-specific metrics such as <a href="https://medium.com/swlh/rank-aware-recsys-evaluation-metrics-5191bba16832">MRR, nDCG</a>, and <a href="https://en.wikipedia.org/wiki/Kendall_rank_correlation_coefficient">Kendall-Tau</a>. In reality, search engines are evaluated using a mix of these metrics, tailored and adjusted according to the specific use case.</p><p>But what if our data in reality is structured differently from the typical output of a classic search engine? What if we follow a case-specific methodology to designate the order of results as significant or relevant that is different from the usual and simple list of results?</p><p>This is precisely the scenario for us at Healthee, where the search service results had to adhere to a “bucketed” structure, rather than just a list. The exact position of a search result item isn’t as critical as its association within a group of results that appear together and their location as a group. Essentially, the importance lies in a search result having a certain rank or being within the same “league” of results.</p><p>Happy users lead to thriving companies, and these users are usually those who receive relevant search results when looking up stuff on your website or app. But who determines the relevance of these results? Your search algorithm might be complex and it could involve one or more of many methods (such as a closed lookup table with <a href="https://towardsdatascience.com/how-fuzzy-matching-improve-your-nlp-model-bc617385ad6b">fuzzy</a> logic or lists with synonyms for simple sets as well as <a href="https://en.wikipedia.org/wiki/Similarity_search#:~:text=Similarity%20search%20is%20the%20most,between%20any%20pair%20of%20objects.">similarity searches</a>, <a href="https://en.wikipedia.org/wiki/Knowledge_graph">knowledge graphs</a>, and other machine learning models for more advanced cases). Regardless of the method used, an independent evaluation method for your search algorithm is essential. This is typically achieved by conducting regular tests using a regularly maintained gold set. You would probably want to trigger this test following a significant data update or at regular time intervals if your data expands steadily over time.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*gjuyg8-pdOtsgBks" /><figcaption>Don’t let your users be like Bono. Let them find what they’re looking for (Image: Midjourney)</figcaption></figure><p>To illustrate, let’s consider a real-world example as it arises in the Healthee App, which aids users in navigating the healthcare system and obtaining information about their coverage for a variety of medical services.</p><p>Imagine a user who wants to find out if they’re covered for an MRI, a service consumed by millions daily. The user enters a search query (simply “MRI” in this instance) and receives a list of results. At this point, our search algorithm acts as a lone sailor at sea, solely reliant on previously acquired skills. However, during the optimization of our search model (whether through machine learning or manual parameter adjustment), we aim to ensure search efficacy.</p><p>In our specific healthcare context, it is doctors who are entrusted with the responsibility of maintaining an accurate <strong><em>gold set</em></strong> that is used to evaluate the search algorithm. Naturally, there are other methods to evaluate searches such as user signals, but this approach is based on the knowledge of a domain expert angle rather than in-app events.</p><p>Back to our results. We searched for “MRI” and got the following:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Vf-vai8C94YntUZcyded8w.png" /></figure><p>Looks good, doesn’t it? All results are with ‘mri’, so.. probably?</p><p>Well, the thing is we don’t really know. Sailor alone at sea, remember? But now let’s help our brave seaman and grant him a precious life vest: the ability to know the real order of the results.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YH5HiHBizhJ480FMVK9jEA.png" /></figure><p>The testing phase plays out precisely like this: our <strong><em>gold set</em></strong> acts as a handy guide, allowing us to understand the correct sequence of results. So, how did we do? The outcome was not exceptional, but not disastrous either; we completely overlooked our initial search result, yet the second to fourth results were correctly ordered and relatively high. The fifth and sixth ones were roughly where they were anticipated to be.</p><p>The good news is that there are already great metrics to measure these results’ accuracy, as mentioned above. We can set a score according to those metrics and evaluate how good our search algorithm is.</p><p>Yet, here lies the challenge: what if we aren’t overly concerned about the precise placement of each search result? After all, perhaps “<strong>advanced imaging — mri — outpatient hospital</strong>” and “<strong>advanced imaging — mri — freestanding radiology facility</strong>” carry equal importance, and a swap in their positions wouldn’t be catastrophic. This dilemma is precisely what our doctors encountered when creating the gold set. If they insist on accuracy linked directly to the index, they might impose an overly rigid rule, which could potentially undermine the search algorithm rather than enhance it.</p><p>So, instead of constructing an index-based gold set, we opted for a ranking-based one.</p><p>Let’s take a glance at the actual gold set we’ve assembled for MRI test results:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*MV7L_kHDTyex3nx7Mh80eg.png" /></figure><p>What we see in our ranking-based gold set is the following:</p><ol><li>The 1st ranking bucket contains only one result.</li><li>The 2nd bucket contains three different results.</li><li>The 3rd bucket contains two results.</li></ol><p>As we have stipulated earlier, the function of the buckets is to eliminate the difference between cases where items from the same rank swap places in the search result. If such a swap occurred, we would consider it acceptable and the accuracy of the test would be the same as in the case the exact gold set was returned.</p><p>Let’s explore an example. Assume that the so-called “perfect set” yields a certain score. The following set would produce the exact same score:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*8rkwbqEgS_LjtBy8EQmejQ.png" /></figure><p>And even the following one would:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XOxmT-BN1WilugQtL5QsAg.png" /></figure><p>But this returned result will come up with a lower score:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*cNOd3aAiwpAH66OUXvHlSQ.png" /></figure><p>And the next one with even a lower one:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*WByO-x2CHMlQvTgdXAW75A.png" /></figure><p>Just to be clear, the search engine itself<strong><em> does not</em></strong> return any rankings. Instead, it simply generates a list of search results based on its own logic. The numbers on the right are merely for our own comprehension of the search engine’s performance. Naturally, these rankings are known to the testing mechanism, and shortly we will dive into how that mechanism operates and how it utilizes the rankings predetermined by our doctors.</p><p>We mentioned the word score before several times. How does this score come to be? What principles lie at the base of such scoring? Answering this question is key to designing a test that will work well with the kind of data we have in our hands (namely, lists of ranked items).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*t8XwvI_Neh7xy-AO" /><figcaption>A mysterious man staring into a dark tunnel of incomprehensible numbers (Image: Midjourney)</figcaption></figure><h3>Building on NDCG</h3><p>We have previously touched upon existing algorithms that measure test results mostly based on the index value of the returned result item. One of them, nDCG (normalized discounted cumulative gain), was initially contemplated for our specific scenario but encountered an obstacle when addressing the ranking concern — specifically when our domain experts started bucketing the source of truth data. Before we could implement it, some modifications would be necessary.</p><p>How can we measure the accuracy of the returned result if the gold set is in buckets? We would have to make an important assumption about the distribution of the results: to assume that their rank structure mirrors that of the gold set. Yes, even though there is nothing inherently “ranky” about our search engine.</p><p>So if our gold set is</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nYXtQyPNOi4MiotXLMigpw.png" /></figure><p>we would extrapolate from it the “assumed ranks” and will apply them to the set of results that the search engine provides, which are devoid of any ranking. Therefore, if our test results look like the following, they would be assigned a set of assumed ranks:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d3opPdSqX_6S2vFfnyG_PA.png" /></figure><p>Now that we have the ability to compare apples to apples, we can use a measuring algorithm and create our metric. For that, we can build on nDCG and re-engineer it to our purpose so ranks are taken into account.</p><p>The function for calculating the gain is</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*pwvNj9w5DSJffx0N" /></figure><p>Let’s break this down:</p><p>In the numerator, we are raising 2 to the power of a number. That number is the lesser value between the true rank and the assumed rank. In the denominator, we’re taking the base-2 logarithm of the greater value between the two. This is done to prevent attributing more significance to the swapping of a higher rank from the gold set with a lower rank from the assumed ranks, or vice versa. In other words, it wouldn’t make a difference to us whether we “swapped up” or “swapped down” — the gain will be the same either way. We add 2 to avert a denominator that’s equal to zero.</p><p>We also need to mention that our doctors are very passionate about the Olympics games. When they set the rankings they did it according to the medals system: gold medal is for first place, silver for the second, etc. However our algorithm actually interprets higher numbers as higher ranks, therefore we need to perform an inversion prior to any calculation:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*b77zTarPqNtzXrhh" /></figure><p>Also, note that rankings are 1-indexed and not 0-indexed.</p><p>Next, what happens during calculation (once we have inferred the assumed ranks) is an iteration through all the search results — which are also in the true relevant set — and calculating the gain each result is contributing to the final score</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9pjrAgHeG0yTY4uO" /></figure><p>where RDCG stands for Ranked Discounted Cumulative Gain, accumulating gain from each <em>i</em>th out of N results.</p><p>(Eagle-eyed readers may spot a potential hiccup, as we’re only making comparisons between known entities — i.e., items that are in the gold set. We’ll address this shortly.)</p><h3><strong>“Can I get a discount, please?”</strong></h3><p>By this point, we’ve developed a clear understanding of the <em>“Ranked”</em> part of the algorithm quite well, as well as the <em>“Cumulative” and</em> “<em>Gain”</em> parts. Let’s focus on <em>“Discounted”</em> for a moment. This component of the algorithm means that the further down we move in our ranking list, the lesser we want the gain to contribute to the accuracy score. This is because a rank at the 15th position is of much less concern to us compared to the 1st or 2nd rank. This principle manifests in two ways: firstly, higher ranks contribute more to RDCG; this is the numerator of the formula. Secondly, a discrepancy between higher ranks is more critical than a discrepancy between lower ranks, because we care more about results that are returned at the top of the list; this is the denominator — specifically the log.</p><p>Let’s explore this concept with some numerical examples (keeping in mind that we’ve had to invert the ranks):</p><p>When <em>true rank</em> is 3 and <em>predicted rank</em> is 4:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*syGJGFTOB0mEqMfU" /></figure><p>And when <em>true rank</em> is 3 and <em>predicted rank</em> is 5:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*k-CHERj_C-ddfyC9" /></figure><p>So we see that the bigger the gap, the lower the gain.</p><p>Additionally, when <em>true rank</em> is 4 and <em>predicted rank</em> is 5:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*_s4eWsvU50DHCpCY" /></figure><p>That is, when the gap is 1 rank just like in the first example, but at higher ranks altogether, then the gain is higher too.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/330/0*Zx992CqIqmkStI2s" /><figcaption>An ambiguous graph that attempts to show the interaction and effect of <em>True Rank </em>(x) and<em> Predicted Rank </em>(y) on the <em>Gain</em> (z) (Image: Wolfram Alpha)</figcaption></figure><p>A crucial step we need to take to compare different queries and their corresponding search results is normalization. Since the gain is simply a result of score addition and is not limited by an upper bound, if we have a big set it is most likely that it will yield a bigger score compared to a smaller set. How do we perform this normalization (i.e., bind the score to a range between 0 and 1)? To do that, we will first calculate the score of the perfect set and normalize it accordingly. Thus:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*lcno1axINtibyh70" /></figure><p>Where NRDCG is Normalized RDCG. Now we have a score that would be interpretable across different queries as well as easier to interpret across time.</p><h3>“Hope of ill gain is the beginning of loss.” — Democritus</h3><p>Let’s consider the following scenario. We have two sets of search results returned from our engine and wish to measure the accuracy. One set is the perfect set where all results are in the best possible order (or more accurately — one of them), and we know it will give us the maximal RDCG. The other set is far from perfect as it returns many false positives interweaved into the perfect set, keeping “good results” in order. What do you think will happen in terms of scoring?</p><p>The “perfect” set:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UexyL-If9fK9E5YNTiV4XA.png" /></figure><p>Set with “infiltrators”:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*G0llskvOGprDz9qfs-Vy0g.png" /></figure><p>One thing that immediately sticks out is that our results set has grown. How does this affect the scoring? (The ranks above are actually not in the structure the algorithm will assume about the test results, we just want to see the “infiltrator” items). The assumed ranks will still get the rank structure of the <em>true set</em> as it “transforms” it (in <a href="https://towardsdatascience.com/what-and-why-behind-fit-transform-vs-transform-in-scikit-learn-78f915cf96fe">Sklearn</a> terms).</p><p>So our assumed ranks will look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zdNPZF3wImpvqW5j3zWwYQ.png" /></figure><p>Congratulations — we have a new rank! Our assumed ranks are indeed reflected in the resulting rank distribution, but they represent only part of it. Because the list grew, some of the items are now <em>out of bounds</em> of the gold set length. Those out-of-bounds items are assigned a new rank (the lowest rank possible) so that when the algorithm iterates over the list it will get the lowest gain when calculated for those items.</p><p>But this is not a loss, exactly, is it? This simply leads to a lower gain. This is why the implementation of the algorithm also allows to control for and penalize two different kinds of situations: one is when “infiltrators” are gettings into the ranks (pun definitely intended) as above, namely, having false positives; and another is when items from the gold set don’t appear at all in the search results, namely, false negatives. We are also able to control whether either of the false positives or false negatives is penalized in proportion to the rank gap or getting a fixed maximal penalty. Penalizing results are not part of nDCG, so this control elevates our algorithm to NRDCGL — Normalized Ranked Discounted Cumulative Gain and Loss.</p><p>Allowing for loss is introducing a new phenomenon to our scoring — it can become negative. While a negative score in itself is not a problem (especially if we measure individual queries in a “set of sets” across time), it can make a complete test of queries less interpretable if you want to average the entire test scores to get a total score between 0 and 1. In order to facilitate a meta-aggregation of a complete search result set accuracy scores (i.e, of many queries and many search results that are associated with those) there is also an implementation of the algorithm that does not allow for negative scores:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*bCT9uZrwUQh8Jpz0" /></figure><p>One more thing to clarify about loss here is that it is not the same as in the sense of <em>loss function</em>. There isn’t any optimization or an objective function to measure deviation from, but simply a dynamic rule-based approach to penalties.</p><p>The disadvantage of NRDCGL is that if there are too many types of differences in the search results, then we might not be able to tell what factor contributed to the increase or decrease of the score. It is kind of like having <a href="https://www.youtube.com/watch?v=QrGrOK8oZG8">too many cooks</a> — you will not find the culprit that ruined the dish. As mentioned above, it is always a good idea to combine several metrics when assessing non-deterministic models, especially such that rely on frequently changing data.</p><p>In a future article, we will go beyond the algorithmic layer and see how to drive a complete test with an annotated dataset and create a quantitative report on Monday.com (or an analytics platform of your choice). The global testing process will reflect the advantages of RaDiCuGLoss by showing how changes made to the data or the algorithm (that manifest themselves in a limited change in the output of the search algorithm), can help us monitor performance.</p><p>RaDiCuGaLoss is publicly available <a href="https://github.com/GetHealthee/RaDiCuGLoss">here</a>. You’re most welcome to contribute! ❤️</p><p><em>Kudos to </em><a href="https://medium.com/u/6f750ba0bf65"><em>Amit BenDor</em></a><em> and </em><a href="https://medium.com/u/4e5ceaedd21d"><em>EladL</em></a><em> for their great help in developing this algorithm 🙌</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cee35c04ddb1" width="1" height="1" alt=""><hr><p><a href="https://tech.healthee.com/radicugloss-cee35c04ddb1">RaDiCuGLoss!</a> was originally published in <a href="https://tech.healthee.com/">Healthee</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>