Phase 0: AI Product Management Foundations · 40 min · Feasibility/Desirability/Viability Triage · Python
When AI Is (and Isn't) the Right Solution
If the correct answer is a single fixed value derivable from a rule, you don't need a model — you need an if-statement.
What you will learn
- Distinguish deterministic problems (fixed, rule-derivable answers) from probabilistic problems (pattern-matching over ambiguous or high-dimensional input)
- Apply a feasibility/desirability/viability triage to decide whether a proposed feature is actually a good fit for AI
- Identify concrete signals that a problem should be solved with a lookup table, rules engine, or heuristic instead of a model
- Identify concrete signals that a problem's input space or judgment requirement genuinely calls for AI
- Explain the real cost — engineering time, reliability, maintenance, user trust — of defaulting to AI on a deterministic problem
The Problem
A checkout team is asked to fix a recurring support complaint: customers are confused about why their shipping cost changes at checkout. Someone on the team proposes "let's train a model to predict shipping cost from the cart and address, so we can show an estimate earlier in the flow." It sounds reasonable in the meeting. It's also completely wrong, because shipping cost here isn't a prediction problem — it's a lookup. Weight, dimensions, destination zone, and carrier rate card are all known, and the exact cost is a deterministic function of those inputs. There is one correct number, and the business already has the formula.
The team builds the model anyway. Three sprints go into collecting training data (LTV of building a dataset from historical checkouts, most of which reflect a rate card that's since changed), tuning a gradient-boosted regressor, and standing up an inference endpoint. It ships. It's right most of the time and wrong in a way nobody can predict — off by $4 on international orders, off by $0.50 on some domestic ones, no clean pattern to the errors because the true relationship was never fuzzy in the first place; the model is just approximating an equation that already existed. Support tickets about shipping cost don't go down, they change shape: now customers see a number, get charged a different number, and conclude the site is buggy or dishonest. Engineering now maintains a model that has to be retrained every time the carrier changes rates, monitored for drift that shouldn't exist, and explained to a confused customer by a support rep who has no idea why the "estimate" was wrong. A fifteen-line function reading from the existing rate table would have been correct 100% of the time, shipped in a day, and never needed retraining.
This mistake is common because "use AI" has become a reflex rather than a decision. The fix isn't a rule against using AI — it's a habit of asking, before any design work starts, whether the problem is actually the kind of problem AI is good at.
Deterministic vs. probabilistic problems
The question underneath every "should this be AI" decision is whether the problem has a single correct answer that's derivable from known rules, or whether it requires pattern-matching over input that's ambiguous, high-dimensional, or genuinely disputed even among humans.
Deterministic problems have a fixed, knowable answer for a given input. "What's the sales tax for this ZIP code" has exactly one correct answer, and it's a lookup against a published rate table — not a pattern to be learned. Billing calculations, tax tables, unit conversions, eligibility rules with clear cutoffs (age >= 18), and inventory counts are all deterministic. If you can write the correct logic as an if/else chain or a table lookup, and two engineers implementing it independently would produce identical outputs, it's deterministic.
Probabilistic problems don't have one clean rule. "Is this support ticket urgent" depends on tone, context, implicit signals, and judgment that reasonable humans would disagree on — there's no lookup table for "urgent," because urgency isn't a fact about the ticket, it's an inference from ambiguous evidence. Fraud likelihood, image content, sentiment, "does this resume match the role" — all probabilistic. The input is unstructured or high-dimensional (free text, images, audio, behavioral sequences), and no finite set of hand-written rules covers it well.
AI earns its cost on probabilistic problems, where a rules engine would either be impossibly large or perpetually incomplete. On deterministic problems, AI adds three costs without adding value: it's less accurate than the rule it's approximating, it's opaque (nobody can point to why it gave a specific answer), and it's more expensive to build, run, and maintain than the logic it replaces.
The feasibility / desirability / viability triage
Once you suspect a problem might be probabilistic enough to justify AI, run it through three questions before committing engineering time:
| Lens | Core question | Red flags |
|---|
| Feasibility | Do we have the data to do this, can a model plausibly perform the task, and what's our error tolerance? | No labeled data and no realistic way to get it; the task requires near-zero error (billing, safety-critical actions) but models are inherently probabilistic; even a strong model would be barely better than a simple heuristic |
| Desirability | Does the user actually want an AI-mediated experience here, or do they want a fast, predictable, deterministic answer? | Users want a guarantee ("exactly how much will I be charged"), not a best guess; an AI answer that's sometimes wrong erodes trust faster than no feature at all; the "AI" framing adds latency or friction to a task users want instant |
| Viability | Is the cost, latency, and maintenance burden of an AI approach actually worth it compared to a simpler approach? | A rules engine or lookup table already solves this at near-zero marginal cost; the model needs constant retraining as the "ground truth" changes; the infra and on-call cost of running inference exceeds the value the feature creates |
A "no" on any one of the three is a real signal, not a tiebreaker to override. Feasible-but-undesirable AI (technically possible, but users want a guarantee) is just as much a rejection as infeasible AI. The most common failure mode in practice is skipping straight to feasibility ("can a model do this?") without ever asking whether it should.
"Can a model do this" is the wrong first question
Almost anything can be framed as a prediction problem if you squint — including sales tax, which you could technically train a model to approximate from ZIP code and cart total. The question that actually matters isn't whether a model could do it, it's whether the problem has a single correct answer that a rule already computes exactly. If it does, a model is a strictly worse version of code you could write today: less accurate, less debuggable, and more expensive to maintain. Ask "is there already a correct rule?" before you ask "can a model learn this?"
Reading the signals
Two checklists, in practice, do most of the work of this decision.
Signals it's NOT an AI problem: the correct answer is a single fixed value derivable from a documented rule; error tolerance is near-zero (billing, tax, legal eligibility, safety interlocks); a lookup table or if/else chain already solves it correctly today; the "AI" would functionally just be doing string or exact-value matching; the input space is small and fully enumerable.
Signals it likely IS a good AI problem: the input is unstructured or high-variance — free text, images, audio, arbitrary user behavior; the mapping from input to output is fuzzy, contextual, or something humans themselves disagree on; there's no practical way to enumerate all the rules (the rule list would be thousands of entries long and still miss cases); small errors are tolerable and recoverable, not catastrophic; the value of a "good enough, most of the time" answer clearly exceeds the value of a slow, perfect one.
A team wants to build a feature that tells customers exactly what their final invoice total will be, including all taxes, discounts, and fees, before they check out. Someone proposes training a model on historical invoices to predict the total. What should a PM say?
Invoice total isn't a pattern to be learned — it's an arithmetic function of tax rates, discount rules, and fee schedules the business already knows exactly. A model trained on historical invoices would at best approximate that function with some error, and any error here is a trust problem: customers who see one number and get charged another will complain regardless of how "close" the model was. This fails feasibility (why approximate something you can compute exactly) and desirability (users want a guarantee, not a probabilistic estimate) at once. The fix is to compute the total directly from the existing rule set, not to train a model on it.
A team wants to automatically route incoming customer emails to the right department (billing, technical support, returns, sales) based on the email's subject and body text. A PM on the team says "we already have keyword rules that route based on subject line, that's good enough, we don't need AI here." Is the PM right?
This is close to the mirror image of the invoice example. Email subject and body are unstructured, high-variance text, and the "correct" department often depends on phrasing, tone, and context that a fixed keyword list will systematically miss ("my card was charged twice" might not contain any of the hand-picked billing keywords). There's no practical way to enumerate every phrasing customers will use, which is exactly the signal that favors AI over a rules engine. The PM's instinct to distrust AI-by-default is good discipline, but applied to the wrong problem here — keyword rules are a reasonable first pass, but they'll plateau in a way a classifier trained on labeled routing examples won't.
Build
Build it
The code artifact lesson_00_02_ai_fit_triage.py implements a feasibility/desirability/viability triage tool for feature proposals:
TriageCriterion — a scored sub-question within one of the three lenses (Feasibility, Desirability, Viability), each answerable 0-2TRIAGE_CRITERIA — the full set of criteria across all three lensesFeatureProposal — a dataclass capturing a proposed feature's name, description, and scored answers to each criterion- A scoring function that aggregates per-lens and overall scores and returns a recommendation: Good AI fit, Marginal — reconsider, or Not an AI problem (with a suggested deterministic alternative)
render_triage_report() — produces a markdown triage report with per-lens scores, the recommendation, and reasoning- Three worked proposals: "Predict shipping cost at cart" (clearly deterministic), "Flag possibly fraudulent transactions" (clearly probabilistic), and "Auto-suggest a support ticket priority" (borderline, scored to show a Marginal result)
"""
lesson_00_02_ai_fit_triage.py
A feasibility / desirability / viability triage tool for deciding whether
a proposed feature is actually a good fit for AI, or whether a lookup
table, rules engine, or simple heuristic would be more accurate, cheaper,
and more debuggable.
This is the artifact an AI PM produces before greenlighting engineering
work on a new AI-flavored feature — it forces the "should this be AI"
question to be answered explicitly and on the record, instead of by
reflex in a planning meeting.
Reference: IDEO's Desirability/Feasibility/Viability framework
(https://www.ideou.com/blogs/inspiration/how-to-balance-desirability-feasibility-and-viability-in-product-innovation)
and Google's People + AI Guidebook
(https://pair.withgoogle.com/guidebook/).
Usage:
python lesson_00_02_ai_fit_triage.py
No external dependencies, no network calls, no API keys required.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
class Lens(Enum):
FEASIBILITY = "FEASIBILITY"
DESIRABILITY = "DESIRABILITY"
VIABILITY = "VIABILITY"
class Score(Enum):
POOR = 0 # strong signal AGAINST an AI approach
MIXED = 1 # ambiguous, could go either way
STRONG = 2 # strong signal FOR an AI approach
@dataclass
class TriageCriterion:
criterion_id: str
lens: Lens
question: str
poor_signal: str
strong_signal: str
# ── The triage criteria ──────────────────────────────────────────────────────
# Nine questions across the three lenses. Each is phrased so a PM can score
# it 0 (poor fit for AI), 1 (mixed/ambiguous), or 2 (strong fit for AI)
# against a specific feature proposal, with the concrete signal that would
# justify each end of the scale.
TRIAGE_CRITERIA: list[TriageCriterion] = [
# FEASIBILITY — data, task difficulty, error tolerance
TriageCriterion(
"data-availability", Lens.FEASIBILITY,
"Do we have (or can we realistically get) labeled data or a clear "
"signal to learn from?",
"No labeled data exists and there's no practical way to collect it.",
"Ample historical data with a clear, learnable label already exists.",
),
TriageCriterion(
"task-is-pattern-matching", Lens.FEASIBILITY,
"Is the task genuinely pattern-matching over ambiguous input, "
"rather than a calculation with one correct answer?",
"The correct output is a single value derivable from a known rule "
"or formula.",
"The correct output depends on context, phrasing, or judgment that "
"varies case to case.",
),
TriageCriterion(
"error-tolerance", Lens.FEASIBILITY,
"Can the product tolerate the error rate a realistic model would "
"have?",
"Error tolerance is near-zero (billing, safety, legal "
"eligibility) — any wrong answer is a serious problem.",
"Occasional wrong or imprecise answers are recoverable and "
"acceptable to users.",
),
# DESIRABILITY — does the user want this experience
TriageCriterion(
"user-wants-a-guarantee", Lens.DESIRABILITY,
"Do users want a fast, guaranteed-correct answer rather than a "
"best guess?",
"Users expect a guarantee (e.g. 'exactly how much will I be "
"charged') and would lose trust in a probabilistic answer.",
"Users are fine with a best-effort answer, especially if it saves "
"them time or effort versus doing it manually.",
),
TriageCriterion(
"wrong-answer-cost-to-trust", Lens.DESIRABILITY,
"If the AI is occasionally wrong, how much user trust does that "
"cost relative to the value it adds?",
"A wrong answer here reads as the product being broken or "
"dishonest, not 'AI being imperfect.'",
"A wrong answer is a minor, easily corrected inconvenience "
"(e.g., mis-routed email gets re-routed).",
),
TriageCriterion(
"ai-framing-adds-friction", Lens.DESIRABILITY,
"Does an AI-mediated experience add unwanted latency or friction "
"to a task users want instant?",
"Users want an immediate, deterministic response and any added "
"latency or uncertainty actively hurts the experience.",
"Users are already waiting on a judgment call a human would "
"otherwise have to make manually — AI removes wait time.",
),
# VIABILITY — cost, maintenance, simpler alternatives
TriageCriterion(
"simpler-alternative-exists", Lens.VIABILITY,
"Does a lookup table, rules engine, or simple heuristic already "
"solve this correctly today?",
"A simple deterministic approach already solves this at near-zero "
"cost and complexity.",
"No simple rule set covers the input space; rules would be "
"thousands of entries long and still miss cases.",
),
TriageCriterion(
"maintenance-burden", Lens.VIABILITY,
"Would an AI approach require ongoing retraining or monitoring "
"that a deterministic approach wouldn't?",
"The 'ground truth' is a fixed, rarely-changing rule set — "
"retraining would just be chasing a target that never moves.",
"The underlying pattern genuinely shifts over time in ways only "
"a retrained model could track (e.g., evolving fraud tactics).",
),
TriageCriterion(
"infra-cost-justified", Lens.VIABILITY,
"Does the value created clearly exceed the infra, latency, and "
"on-call cost of running inference?",
"The feature's value is low or the deterministic alternative is "
"nearly free, making inference cost hard to justify.",
"The task would otherwise require significant manual human "
"review time that the model meaningfully reduces.",
),
]
@dataclass
class FeatureProposal:
name: str
description: str
answers: dict[str, Score] = field(default_factory=dict)
def score_for_lens(self, lens: Lens) -> tuple[float, int]:
items = [c for c in TRIAGE_CRITERIA if c.lens == lens]
scored = [self.answers.get(c.criterion_id, Score.POOR) for c in items]
if not scored:
return 0.0, 0
total = sum(s.value for s in scored)
max_total = len(scored) * Score.STRONG.value
return (total / max_total) * 100, len(scored)
def overall_score(self) -> float:
lens_scores = [self.score_for_lens(lens)[0] for lens in Lens]
return sum(lens_scores) / len(lens_scores)
def weakest_lens(self) -> tuple[Lens, float]:
scored = [(lens, self.score_for_lens(lens)[0]) for lens in Lens]
return min(scored, key=lambda pair: pair[1])
def recommendation(proposal: FeatureProposal) -> str:
"""A hard floor on any single lens matters as much as the overall score:
a proposal that's strong on Feasibility and Viability but fails
Desirability (users want a guarantee) is still a bad AI fit overall."""
lens_scores = {lens: proposal.score_for_lens(lens)[0] for lens in Lens}
overall = proposal.overall_score()
if any(score < 35 for score in lens_scores.values()):
return "Not an AI problem — use a deterministic alternative instead"
if overall >= 70:
return "Good AI fit"
if overall >= 45:
return "Marginal — reconsider scope or gather more evidence"
return "Not an AI problem — use a deterministic alternative instead"
def render_triage_report(proposal: FeatureProposal) -> str:
overall = proposal.overall_score()
rec = recommendation(proposal)
weakest_lens, weakest_score = proposal.weakest_lens()
lines = [
f"# AI Fit Triage — {proposal.name}",
"",
f"**Proposal:** {proposal.description}",
"",
f"**Overall score:** {overall:.0f}/100",
f"**Recommendation:** {rec}",
"",
"## Scores by lens",
"",
"| Lens | Score | Criteria scored |",
"|---|---|---|",
]
for lens in Lens:
score, n = proposal.score_for_lens(lens)
lines.append(f"| {lens.value.title()} | {score:.0f}/100 | {n} |")
lines += [
"",
f"**Weakest lens:** {weakest_lens.value.title()} ({weakest_score:.0f}/100)",
"",
"## Criterion detail",
"",
]
for lens in Lens:
lines.append(f"### {lens.value.title()}")
for c in [c for c in TRIAGE_CRITERIA if c.lens == lens]:
score = proposal.answers.get(c.criterion_id, Score.POOR)
lines.append(f"- **[{score.name}]** {c.question}")
if score == Score.POOR:
lines.append(f" - Signal seen: {c.poor_signal}")
elif score == Score.STRONG:
lines.append(f" - Signal seen: {c.strong_signal}")
lines.append("")
return "\n".join(lines).rstrip() + "\n"
# ── Worked sample proposals ──────────────────────────────────────────────────
# Three feature proposals scored through the triage: one clearly
# deterministic, one clearly probabilistic, and one genuinely borderline.
SHIPPING_COST_ANSWERS: dict[str, Score] = {
"data-availability": Score.STRONG,
"task-is-pattern-matching": Score.POOR,
"error-tolerance": Score.POOR,
"user-wants-a-guarantee": Score.POOR,
"wrong-answer-cost-to-trust": Score.POOR,
"ai-framing-adds-friction": Score.POOR,
"simpler-alternative-exists": Score.POOR,
"maintenance-burden": Score.POOR,
"infra-cost-justified": Score.POOR,
}
FRAUD_FLAG_ANSWERS: dict[str, Score] = {
"data-availability": Score.STRONG,
"task-is-pattern-matching": Score.STRONG,
"error-tolerance": Score.MIXED,
"user-wants-a-guarantee": Score.MIXED,
"wrong-answer-cost-to-trust": Score.MIXED,
"ai-framing-adds-friction": Score.STRONG,
"simpler-alternative-exists": Score.POOR,
"maintenance-burden": Score.STRONG,
"infra-cost-justified": Score.STRONG,
}
TICKET_PRIORITY_ANSWERS: dict[str, Score] = {
"data-availability": Score.MIXED,
"task-is-pattern-matching": Score.STRONG,
"error-tolerance": Score.MIXED,
"user-wants-a-guarantee": Score.MIXED,
"wrong-answer-cost-to-trust": Score.MIXED,
"ai-framing-adds-friction": Score.MIXED,
"simpler-alternative-exists": Score.MIXED,
"maintenance-burden": Score.MIXED,
"infra-cost-justified": Score.MIXED,
}
def build_sample_proposals() -> list[FeatureProposal]:
return [
FeatureProposal(
name="Predict shipping cost at cart",
description=(
"Train a model to estimate shipping cost from cart weight, "
"dimensions, and destination so customers see a number "
"earlier in checkout."
),
answers=dict(SHIPPING_COST_ANSWERS),
),
FeatureProposal(
name="Flag possibly fraudulent transactions",
description=(
"Score incoming transactions in real time for fraud "
"likelihood using behavioral and device signals, routing "
"high-risk ones to manual review."
),
answers=dict(FRAUD_FLAG_ANSWERS),
),
FeatureProposal(
name="Auto-suggest a support ticket priority",
description=(
"Suggest an urgency level (low/medium/high) for incoming "
"support tickets based on subject and body text, as a "
"default an agent can override."
),
answers=dict(TICKET_PRIORITY_ANSWERS),
),
]
if __name__ == "__main__":
for proposal in build_sample_proposals():
print(render_triage_report(proposal))
print()
print(
"Takeaway: shipping cost fails all three lenses because it's a "
"deterministic calculation, not a pattern — a lookup table wins on "
"accuracy, cost, and trust. Fraud flagging scores well because the "
"input is genuinely high-variance and the rule set to catch it would "
"never be complete. Ticket priority lands in the Marginal band: it's "
"plausible as an AI feature, but the team should tighten scope "
"(e.g. suggestion + human override, not silent auto-routing) before "
"committing engineering time."
)
Unlock the full lesson
You've read the first 2 sections. The rest of this lesson covers Check Yourself — plus a hands-on lab, quiz, and project artifact.
Create a free account — the opening phases of 29 of 35 courses are free, no credit card.
Browse all courses · View pricing · DeVenture Academy