Product Design · AI Systems
I find what a product is missing, design it, and build it.
Articles
Selected Projects
A pre-call brief that turns a patient's scattered history into one page a telehealth doctor can read in thirty seconds, built to stay silent rather than guess.
Read the case study
A retrieval-augmented generation system built from scratch around one principle: an answer is only worth returning if it can be verified against its sources.
View project
Four specialist agents attack, defend, and pressure-test your decision. No validation. Just clarity.
View project
A Model Context Protocol server surfacing live AI research and Hacker News directly inside Claude sessions.
View projectSnap a meal, know the calories. An AI-powered iOS calorie counter built around awareness, not perfection.
View project
Algorithmically generated visual simulations. Physics-based motion, mathematical rhythm, visual harmony.
View project
About
I came up through biology, which is really the study of systems: how small things upstream shape everything downstream, and why something behaves the way it does once you look closely enough.
That curiosity never switched off. It moved into data, then into building. I think about how information is structured, what gets lost before it ever reaches a model, and how a quiet decision early on becomes a hard limit later.
So I research a problem until I understand it, then I build. Health is where I go deepest, because it is the world I came from and the one I read fastest.
I also enjoy
Wildlife
Running
Nature
A brief that gives a telehealth doctor the patient's story before the call begins.
Telehealth fixed the hardest part of seeing a doctor: getting in front of one. What it did not fix is what the doctor walks in with. They join the call carrying whatever the system handed them, usually a short reason for the visit and a record they technically have access to but no time to read. So the conversation begins on the surface, with the doctor asking questions the history could already answer, while the detail that actually matters stays buried in a file that stays closed.
The strange thing is that the information is almost always there. It is just in the wrong shape. It arrives raw and scattered, a stack of past visits and notes, handed to someone who has minutes, not hours, and a full day of calls behind this one. Having the data and being able to use it in the moment are two different problems, and telehealth solved the first while leaving the second wide open.
That gap is what the Health Card is for. Most of the recent work in this space looks backward: ambient scribing sits in the call and writes the note afterward, which is real and valuable. The Health Card looks the other way. It works in the moments before the call begins, turning everything the platform already knows into a single page the doctor can absorb at a glance, so they arrive understanding the person instead of meeting them cold.
What makes that page useful is not that it lists more, but that it connects. It reads the history and surfaces the thread worth noticing. Three visits for the same complaint in a month are not three separate events; they are one pattern, and a doctor who sees the pattern makes a different decision than one who sees a single isolated visit. The card does the connecting so the doctor can spend their attention on the patient rather than on reconstruction.
Underneath all of it sits one decision that shaped everything else: what the card does when it does not know. The answer is that it says nothing. A field it cannot fill stays empty. It never reaches for a plausible guess to look complete, because in healthcare a confident invention is not a small error. One fabricated medication does not cost you one fact. It costs you the doctor, for good. Clinicians extend trust slowly and withdraw it instantly, and a tool caught inventing something once is a tool that never gets opened again. So the card is built to be quiet and correct rather than full and impressive.
That principle turns out to be a design problem before it is an engineering one. A clinician decides whether a tool is worth trusting in the first few seconds, so the work was in earning that judgment: showing the source behind every flag so it can be checked, letting the page be honest about the edges of what it knows, choosing clarity over density on a screen meant to be read in a hurry. Trust here is not something you assert. It is something the interface has to behave its way into.
And the card is not meant to be a static snapshot. The natural next step is to close the loop with the work that already happens after a call. The brief informs the visit, the visit produces notes, and those notes feed back so the next brief is sharper than the last. Over time the record and the moment of care stop being two disconnected things and start improving each other.
I built a working prototype of this. The code was the least interesting part of it. The real work, the part I cared about, was deciding what a doctor actually needs in the first thirty seconds of a call, and what a system in healthcare should have the discipline to refuse to say.
Self-directed project · Working prototype on simulated data
A retrieval-augmented generation system built from scratch, designed around one principle: an answer is only worth returning if it can be verified against its sources.
Built without frameworks: a query router that classifies intent before touching the vector store, hybrid retrieval combining dense embeddings and BM25 merged with Reciprocal Rank Fusion, and a SELF-RAG critique loop that scores every claim against source chunks and re-retrieves for anything unsupported.
An eval harness measures routing accuracy, faithfulness, and answer quality. Deployed on the FastAPI official documentation.
Most AI tools agree with you. This one doesn't. Decision Adversary is a multi-agent system built around a single idea: the best way to stress-test a decision is to put it in front of specialists who disagree with each other.
An orchestrator reads your input first, classifies the decision type, and assigns the right domain expert. Then four adversarial agents run in sequence — one attacks every weakness, one steelmans the strongest case, one surfaces what you're not seeing, and one brings sharp domain expertise. A fifth synthesizes everything into a verdict: proceed, proceed with caution, or don't proceed — with the critical factors and one concrete next step.
Snap a meal, know the calories. Calvio is an AI-powered iOS calorie counter built with SwiftUI and the OpenAI Vision API.
Most tracking apps fail for the same reason: they ask too much. Precise weights, manual entries, complicated databases. Calvio was designed around a different assumption — people want awareness, not perfection. A fast photo, a realistic estimate, and a calm interface you will actually open tomorrow.
Built entirely from scratch: SwiftUI interface with Core Data for local storage, OpenAI Vision for food recognition, and a daily log designed to feel like a conversation rather than a spreadsheet.
A Model Context Protocol server that pulls live AI research papers and Hacker News discussions directly into any Claude session.
MCP is Anthropic's open standard for connecting AI models to external data sources. This server implements the full protocol, giving Claude real-time access to the AI research landscape without manual searching or context switching. The right information, at the right moment, inside the tools you're already using.
Built with Node.js and the Anthropic MCP SDK. Connects to the ArXiv API for research papers and the Hacker News Algolia API for community discussions. Runs locally via stdio — no server, no accounts, no setup beyond one config line.
A series of algorithmically generated visual simulations built in Python and published as YouTube Shorts.
Physics-based motion, mathematical loop structures, and color palettes chosen for visual harmony. Each simulation is built from scratch, tuned through iteration, and rendered as short-form video.
Most RAG tutorials give you the same thing: load some documents, embed them, store them in a vector database, query with cosine similarity, pass the results to an LLM, done. It works. It also fails in ways that only become obvious when you try to measure it.
I spent time building an agentic RAG system without LangChain, without LlamaIndex, without any framework abstracting the parts that actually matter. This is what I learned.
The honest answer is that frameworks hide the failure modes. When your RAG system gives a wrong answer, LangChain does not tell you whether the problem was in the chunking, the retrieval, the ranking, or the generation. You have a black box that occasionally lies, and no way to know which part is responsible.
Building from scratch forces you to understand each layer independently. It also means you can measure each layer independently, which turns out to be the entire point.
Ingestion splits documents by sentence boundary with token overlap. A fact that lands at the boundary of two chunks will appear in at least one complete chunk. Without overlap, you lose context at every split.
Hybrid retrieval runs dense semantic search via sentence-transformers in Qdrant and BM25 keyword search in parallel, merged using Reciprocal Rank Fusion. 15 lines of Python. Consistently outperforms either method alone.
Query routing classifies every query before retrieval runs. Some questions are answerable from general knowledge — retrieving for them wastes latency and adds noise. Routing accuracy on the eval dataset: 100%.
Generation uses a grounded prompt: answer only from provided context, be direct. The default LLM behavior produces confident-sounding answers even when context does not support them.
The critique loop is where the system earned the word "agentic."
The SELF-RAG critique loop did not behave the way I expected. Running one API call per sentence meant a five-sentence answer triggered five critique calls. The latency was not acceptable.
The fix was batching. A single API call now scores all sentences at once. One call instead of five. Same grounding quality, fraction of the latency. This pattern shows up everywhere in production AI systems and almost nowhere in tutorials: the naive implementation of a correct idea is often too slow to be useful. The optimization is not an afterthought — it is part of the design.
The eval harness. Not because it is technically complex. What was hard was writing the ground truth questions. Good eval questions are specific enough to have a correct answer, varied enough to stress different parts of the system, and honest enough to catch the failure modes you are trying to measure.
The eval harness is the part of this project that most closely resembles what AI engineers actually do at companies building production systems. Not the retrieval architecture. Not the critique loop. The measurement infrastructure that tells you whether any of it is working.
Final numbers: Routing accuracy 100% · Answer quality 4.3/5 · Naive RAG baseline 3.1/5. The agentic loop accounts for the difference.
Building RAG without frameworks is not about being contrarian. It is about being able to answer the question that matters in any real deployment: why is this wrong, and which part do I fix?
A framework gives you a working system quickly. It does not give you the ability to debug it, measure it, or improve specific components without touching everything else. For a demo, that is fine. For a system that needs to be reliable, it is not.
The repo is at github.com/HadithSalahshour/agentic-rag. The corpus is the FastAPI official documentation.
Stack: Python, Qdrant, sentence-transformers, rank_bm25, FastAPI, Claude API. No LangChain. No LlamaIndex.
Claude has a knowledge cutoff. Everyone who uses it regularly knows this, and most people work around it — rephrasing questions, adding context manually, accepting that some answers will be stale. I did the same thing for a while.
MCP changed that for me. Not as a concept but as something I actually built and used.
Model Context Protocol is a standardized way for Claude to call external tools at runtime. When a connector is active and you ask Claude something relevant, it calls the appropriate tool, receives live data back, and reasons over it inside the same conversation. You do not have to instruct it. You just ask.
I built a server with three tools. One fetches recent AI papers from ArXiv by topic. One pulls top AI and engineering discussions from Hacker News. One combines both into a single digest. When I open Claude in the morning and ask what is worth reading today, it calls the digest tool, gets live data from both APIs, and synthesizes a response. No copy-pasting, no tab switching, no manually feeding it context.
Claude is the reasoning layer. The MCP server is the data layer. Claude decides which tool fits the question, calls it, receives structured text back, and thinks with it. The server does not do any reasoning. It fetches, formats, and returns. Everything that makes the response useful happens on Claude's side.
This distinction matters when you are deciding what your tools should return. I spent more time on the format of each response than on the API calls themselves. Too much data and Claude buries the signal. Too little and it cannot do anything useful with it. The server is simple. Getting the output shape right takes iteration.
MCP servers run in two modes. Stdio runs the server as a local process on your machine, communicating with Claude Desktop through standard input and output. HTTP runs it as a web server that Claude calls over the network.
I built the stdio version first. It connected to Claude Desktop and worked immediately. Then I converted it to HTTP and deployed it to Railway because I wanted to share it with someone else. That is where I hit a real constraint.
Claude Desktop version 1.8555.2 does not support the type http config format for MCP servers. I had a working Railway deployment, a live URL, a green status indicator, and a Claude Desktop that could not connect to it. The error was not obvious. The stdio version I had running locally was working the whole time.
HTTP transport for MCP is real and the deployment path exists. The desktop client support is just not there yet in the current release. If you are building for your own daily use, stdio is the reliable path right now.
The knowledge cutoff is still there. What changed is that it no longer defines the boundary of the conversation.
I ask Claude something now and it has access to what was published this morning on ArXiv and what engineers are actually running into today on Hacker News. It connects that to whatever I am working on. That synthesis is still Claude doing the thinking. The server just makes sure it has something current to think with.
The code is on GitHub if you want to build from it or take it apart.
Stack: Node.js, Anthropic MCP SDK, ArXiv API, Hacker News Algolia API. Runs via stdio.
The best decisions I have watched people make happened in rooms with disagreement in them. Someone argued for the idea. Someone argued against it. Someone asked the question nobody wanted to answer. By the time the decision was made, it had been tested from enough angles that the person making it actually understood what they were choosing.
Most decisions do not happen in rooms like that. They happen alone, or with people who are too close to the problem, or too polite to say the difficult thing. The decision gets made anyway. The blind spots travel with it.
Decision Adversary is a system built for exactly that situation.
Before any analysis begins, the system runs a classification step. One model reads the input, determines what kind of decision it is looking at, and assigns a specialist role to the domain expert agent. This happens before the adversarial work starts, and it changes everything that follows.
A career decision and a product decision can be written in nearly identical language. The words look the same. But the relevant failure modes are different, the useful frameworks are different, and the expertise that matters is different. Without a routing step, a multi-agent system applies the same lens to everything. The orchestrator exists to prevent that.
This step does not appear in the output. It runs silently before anything you see. It is also the reason the analysis feels like it was written for your situation rather than assembled from a template.
After classification, four agents run in sequence. Each one receives only the original input. They do not share intermediate outputs. They do not know what the others concluded.
This constraint is deliberate. When agents share outputs mid-process, they converge. The second anchors on the first. The third adjusts toward both. What looks like four perspectives turns out to be one perspective that iterated on itself. Sequential isolation prevents this. The Devil's Advocate attacks the decision without knowing what the Defender will say. The Defender makes the strongest honest case for proceeding without access to the attack. The Blind Spot Detector looks for what is absent in the input without being anchored by either framing.
The fourth agent brings domain expertise shaped by the orchestrator's classification. Its analysis is calibrated to the specific type of decision, not the general shape of the input.
The fifth model is the only one that sees all four outputs. It reads genuine disagreement — four independent analyses that had no access to each other — and produces a verdict, a ranked set of critical factors, and one concrete next step.
Not considerations. Not things to reflect on. One action. The next step is the hardest part of the synthesis to get right. A verdict without a following action is just an opinion. The system is built to end in something specific enough to actually do.
The third agent is the hardest to evaluate because its subject matter does not appear in the input. Blind spots are, by definition, absent. The agent has to reason about what is not there: the assumptions being made without being stated, the dependencies being treated as stable that are actually contingent, the things the person writing the input did not know they were not thinking about.
When it works, it produces the most valuable output in the system. It finds the thing that would have traveled unexamined all the way to the consequences.
A vague decision produces general analysis. A specific one produces something usable. The system does not compensate for underspecified inputs. It reflects the quality of what it receives.
This turns out to be useful before the analysis even runs. The act of writing a decision precisely enough for the system to take seriously forces a kind of clarity. You find out quickly what you actually know versus what you were treating as settled.
The system does not decide for you. It shows you what your decision looks like from four independent directions at once. What you do with that is still yours.
Stack: React, Anthropic API, multi-agent orchestration. No shared state between agents.