Skip to content

Commit dfd22bd

Browse files
docs: define the v1 /predict API contract
Fixed JSON contract for /health, /predict, /substitutes β€” derived from training/predict.py. Unblocks api + ui to build in parallel.
1 parent 8d9a0a7 commit dfd22bd

2 files changed

Lines changed: 132 additions & 0 deletions

File tree

β€Ždocs/API-CONTRACT.mdβ€Ž

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# API Contract (v1)
2+
3+
The fixed JSON contract the **.NET API exposes** and the **React workbench consumes**.
4+
Both sides build against *this document* β€” it is the single source of truth for the
5+
request/response shapes. It is derived from the reference implementation in
6+
[`training/predict.py`](../training/predict.py); the .NET API ports that logic and its
7+
output **must match these shapes** (verified molecule-for-molecule against Python).
8+
9+
## Conventions
10+
11+
- JSON, UTF-8. Field names are stable; the UI must **ignore unknown fields** (so
12+
additive changes don't break it).
13+
- **Confidence tiers** β€” every value is derived one of these ways, and the UI shows
14+
the tier so nothing reads as more certain than its source:
15+
`computed` (exact from structure) Β· `trained` (ML on open data) Β· `rule`
16+
(deterministic structural rule) Β· `estimate` (published QSPR, known error) Β·
17+
`lookup` (from a loaded reference table) Β· `qualitative` (a class/flag, not a number).
18+
- **Errors** return an HTTP 4xx/5xx status with body `{ "error": "<message>" }`.
19+
- **Optional fields** (`sweet_intensity`, `physchem.measured`, `aroma`, `known_tastes`)
20+
appear only when their model/table/flag is available β€” consumers must tolerate absence.
21+
22+
> Example values below are **illustrative** (shape, types, and tiers are what's
23+
> normative β€” not the specific numbers).
24+
25+
---
26+
27+
## `GET /health`
28+
Liveness probe.
29+
30+
**200** β†’ `{ "status": "ok" }`
31+
32+
---
33+
34+
## `POST /predict`
35+
Single-molecule flavor read.
36+
37+
**Request**
38+
```json
39+
{ "input": "vanillin" }
40+
```
41+
- `input` *(string, required)* β€” a compound **name** or a **SMILES** string. Resolved
42+
as SMILES if parseable, otherwise looked up by name.
43+
- Unresolvable input β†’ **422** `{ "error": "couldn't resolve '<input>' to a structure" }`.
44+
45+
**Response 200** *(tier in parentheses)*
46+
```json
47+
{
48+
"smiles": "O=Cc1ccc(O)c(OC)c1", // canonical SMILES (computed)
49+
50+
"sweet": 0.12, // probability 0–1 (trained)
51+
"bitter": 0.74, // (trained)
52+
"umami": 0.03, // (trained)
53+
"sweet_intensity": 1.8, // vs sucrose β€” OPTIONAL (trained)
54+
"sour": false, // (rule)
55+
"sour_reason": [], // acidic groups matched (rule)
56+
"salty": false, // (rule)
57+
"salty_reason": "no alkali-salt structure", // (rule)
58+
"known_tastes": ["bitter"], // OPTIONAL, verified dataset labels (lookup)
59+
"multitaste": false, // 2+ taste heads β‰₯ 0.5 (trained-derived)
60+
61+
"physchem": {
62+
"computed": { "mol_weight": 152.15, "logP": 1.21, "tpsa": 46.5,
63+
"h_bond_donors": 1, "h_bond_acceptors": 3,
64+
"rotatable_bonds": 2, "aromatic_rings": 1, "heavy_atoms": 11 },
65+
"estimate": { "logS": -1.6, "note": "ESOL estimate (log mol/L), ~0.7 log RMSE" },
66+
"qualitative": { "aroma_volatility": "middle", "volatility_note": "...",
67+
"ionizable_groups": [ { "group": "phenol", "typical_pKa": "9.8–10.3",
68+
"character": "weak acid" } ] },
69+
"measured": { "boiling_point_c": 285, "source": "loaded property table" } // OPTIONAL (lookup)
70+
},
71+
72+
"stability": { "oxidation_watch": [], "hydrolysis_watch": [],
73+
"photodegradation_watch": [],
74+
"note": "qualitative 'watch for' flags β€” not a shelf-life prediction" },
75+
76+
"chemesthesis": { "classes": [], "note": "curated structural class flags, qualitative" },
77+
78+
"analytical": { "retention_index": { "kovats_ri": null, "note": "needs a trained RI QSPR" } },
79+
80+
"labeling": { "eu_declarable_allergen": false, "allergen_name": null,
81+
"note": "EU fragrance-allergen labeling list (curated subset)" },
82+
83+
"safety": {
84+
"disclaimer": "Taste/aroma prediction only ...",
85+
"scope": "Taste/aroma only β€” not a safety/toxicity/GRAS/stability determination.",
86+
"structural_alerts": [], // caution prompts, may be empty (rule)
87+
"gras_status": "unverified for food use", // (lookup, data-gated)
88+
"review_required": true,
89+
"ttc_hint": { "preliminary_tier": "low", "drivers": { "alerts": [], "uncommon_elements": [] },
90+
"note": "PRELIMINARY heuristic, not validated Cramer/TTC" } // (qualitative)
91+
},
92+
93+
"aroma": { "descriptors": [ { "label": "sweet", "intensity": 0.0 } ],
94+
"note": "..." } // OPTIONAL β€” only when requested AND the model is trained (trained)
95+
}
96+
```
97+
98+
**Field notes for implementers**
99+
- Taste-head keys (`sweet`/`bitter`/`umami`) are present **per trained classifier**; a
100+
head below the data threshold is absent and the corresponding rule/flag covers it.
101+
- `sweet_intensity` and `physchem.measured` appear only when their model/table is loaded.
102+
- `salty` may be overridden to `true` with `salty_reason: "verified (dataset label)"` when
103+
a ground-truth label exists (lookup beats rule).
104+
- `aroma` is included only when the request opts in **and** the OpenPOM model is trained;
105+
until then it is omitted (or returns an honest "not trained yet" marker).
106+
107+
---
108+
109+
## `POST /substitutes`
110+
Nearest-neighbor substitution search ("approved ingredients closest to this one").
111+
112+
**Request**
113+
```json
114+
{ "input": "vanillin", "k": 8 } // k optional, default 8
115+
```
116+
117+
**Response 200**
118+
```json
119+
{ "neighbors": [
120+
{ "smiles": "O=Cc1ccc(O)cc1", "similarity": 0.83, "known_tastes": ["bitter"] }
121+
] }
122+
```
123+
- **Today:** Morgan-fingerprint Tanimoto similarity (runs without the aroma model).
124+
- **Later:** upgrades to the learned aroma-embedding space (pgvector cosine) once the
125+
aroma model + embeddings exist β€” **same response shape**, better neighbors.
126+
127+
---
128+
129+
## Versioning
130+
This is **v1**. Additive changes (new optional fields) are non-breaking; consumers
131+
ignore unknown fields. Breaking changes bump the version and are coordinated via PR.

β€Ždocs/README.mdβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ Architecture, capability catalogue, and design decisions.
66
- `CAPABILITIES.md` β€” what the tool does, what's buildable on open data, and the
77
honest limits of public-data prediction
88
- `SOURCES.md` β€” data sources, libraries, research, and license attribution
9+
- `API-CONTRACT.md` β€” the fixed JSON contract the .NET API exposes and the React UI consumes
910

1011
These document *what we're building and why*, so decisions don't get re-argued.

0 commit comments

Comments
Β (0)