docs(api): document the medication natural-language extract route

Wires the v1.5 Coach-backed extractor route into the OpenAPI
registry with field descriptions covering the citation-guarded
return shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-28 10:23:28 +02:00
1 parent 1cc6f500bb
commit a2de3c7a30
2 files changed
+244

No files matched your search

+166
View File
@@ -855,6 +855,44 @@ paths:
$ref: "#/components/schemas/ErrorEnvelope"
"422": *a2
"429": *a3
/api/medications/extract:
post:
tags:
- Medications
summary: Extract scheduling fields from a free-text medication description
description: Runs the user's free-text description through the Coach provider chain and returns a citation-guarded
partial payload the wizard merges onto whatever the user already typed. `name` and `dose` are dropped when not
substring-matched in the original text so the wizard cannot land a hallucinated brand or dose. `cadenceKind` /
`doseUnit` / `weekdays` are closed enums; numeric fields are clamped. Rate-limited 10 requests / 5 minutes /
user, gated against the daily Coach token budget.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/MedicationExtractRequest"
responses:
"200":
description: Citation-guarded partial extraction.
content:
application/json:
schema:
$ref: "#/components/schemas/MedicationExtractResponse"
"401": *a1
"422": *a2
"429": *a3
"502":
description: Upstream provider returned an empty, unparseable, or off-schema reply.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"503":
description: No AI provider configured for the calling user (or operator).
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
/api/medications/{id}/cadence:
get:
tags:
@@ -1875,6 +1913,35 @@ components:
description: Per-medication intake log body. Idempotent via `idempotencyKey`; the server also dedupes by a 60-second
sliding window when the key is absent. Non-skipped intakes auto-decrement pen inventory (best-effort), refresh
the per-day compliance rollup, and — for one-shot medications — flip `active` to false.
MedicationExtractRequest:
type: object
properties:
text:
type: string
minLength: 1
maxLength: 2000
description: Free-text medication description (any locale). Up to 2 000 characters. The model never echoes the text back
into another tenant — it is only used to produce the structured fields.
locale:
description: Optional UI locale hint for the model.
type: string
enum:
- en
- de
- es
- fr
- it
- pl
today:
description: "Optional override of the reference date used to resolve relative phrases (\"tomorrow\", \"next Monday\").
Format: `YYYY-MM-DD`. Defaults to the server's UTC day."
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
required:
- text
description: Free-text medication description payload. The route runs the text through the Coach provider chain and
returns a partial structured payload the wizard merges. Rate-limited 10 requests / 5 minutes / user;
budget-gated against the daily Coach token ceiling.
DisableCoachFlag:
type: object
properties:
@@ -3016,6 +3083,105 @@ components:
- data
- error
additionalProperties: false
MedicationExtractResponse:
type: object
properties:
data:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 100
dose:
type: string
minLength: 1
maxLength: 50
doseUnit:
type: string
enum:
- mg
- ml
- iu
- tablets
- drops
- puffs
- sprays
cadenceKind:
type: string
enum:
- daily
- weekdays
- everyNWeeks
- monthly
- everyNMonths
- yearly
- rolling
- oneShot
weekdays:
maxItems: 7
type: array
items:
type: string
enum:
- MO
- TU
- WE
- TH
- FR
- SA
- SU
intervalWeeks:
type: integer
minimum: 1
maximum: 52
intervalMonths:
type: integer
minimum: 1
maximum: 12
dayOfMonth:
type: integer
minimum: 1
maximum: 31
rollingIntervalDays:
type: integer
minimum: 1
maximum: 365
timesOfDay:
maxItems: 12
type: array
items:
type: string
pattern: ^([01]\d|2[0-3]):[0-5]\d$
startsOn:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
endsOn:
type: string
pattern: ^\d{4}-\d{2}-\d{2}$
oneShot:
type: boolean
confidence:
type: object
propertyNames:
type: string
additionalProperties:
type: number
minimum: 0
maximum: 1
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
GetMedicationCadenceResponse:
type: object
properties:
+78
View File
@@ -43,6 +43,7 @@ import {
MEDICATION_CATEGORY_VALUES,
MEDICATION_TREATMENT_CLASS_VALUES,
} from "@/lib/validations/medication";
import { medicationExtractionSchema } from "@/lib/ai/coach/medication-extract-prompt";
/**
* Common envelopes — every HealthLog API response wraps payload in
@@ -746,6 +747,45 @@ const insightsComprehensiveResponse = z
"AI-generated insights bundle. Strict-schema validated server-side; Coach-routed when the insight surface needs day-level grounding.",
});
// v1.5.0 — natural-language medication extraction route. The wizard's
// optional "Beschreiben" overlay POSTs a free-text description and
// receives a partial structured payload the form merges onto whatever
// the user already typed. Citation-guarded (`name` and `dose` are
// dropped when not substring-matched in the original text) and
// closed-enum-validated.
const medicationExtractRequest = z
.object({
text: z
.string()
.min(1)
.max(2000)
.describe(
"Free-text medication description (any locale). Up to 2 000 characters. The model never echoes the text back into another tenant — it is only used to produce the structured fields.",
),
locale: z
.enum(["en", "de", "es", "fr", "it", "pl"])
.optional()
.describe("Optional UI locale hint for the model."),
today: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/)
.optional()
.describe(
"Optional override of the reference date used to resolve relative phrases (\"tomorrow\", \"next Monday\"). Format: `YYYY-MM-DD`. Defaults to the server's UTC day.",
),
})
.meta({
id: "MedicationExtractRequest",
description:
"Free-text medication description payload. The route runs the text through the Coach provider chain and returns a partial structured payload the wizard merges. Rate-limited 10 requests / 5 minutes / user; budget-gated against the daily Coach token ceiling.",
});
medicationExtractionSchema.meta({
id: "MedicationExtractionResult",
description:
"Citation-guarded partial extraction of medication scheduling fields. Every field is optional; the wizard merges what is present onto the form state and leaves the rest blank. `name` and `dose` are post-validated against the original free-text and dropped when not substring-matched, so the wizard cannot silently land a hallucinated brand or dose. `cadenceKind` / `doseUnit` / `weekdays` are closed enums; numeric fields are clamped to the wizard's wire bounds.",
});
// ── Standard 401 / 422 / 429 responses ───────────────────────────────
const stdResponses = {
@@ -1237,6 +1277,44 @@ export const openApiPaths: NonNullable<ZodOpenApiObject["paths"]> = {
},
},
},
"/api/medications/extract": {
post: {
tags: ["Medications"],
summary: "Extract scheduling fields from a free-text medication description",
description:
"Runs the user's free-text description through the Coach provider chain and returns a citation-guarded partial payload the wizard merges onto whatever the user already typed. `name` and `dose` are dropped when not substring-matched in the original text so the wizard cannot land a hallucinated brand or dose. `cadenceKind` / `doseUnit` / `weekdays` are closed enums; numeric fields are clamped. Rate-limited 10 requests / 5 minutes / user, gated against the daily Coach token budget.",
requestBody: {
required: true,
content: {
"application/json": { schema: medicationExtractRequest },
},
},
responses: {
"200": {
description: "Citation-guarded partial extraction.",
content: {
"application/json": {
schema: dataEnvelope(
medicationExtractionSchema,
"MedicationExtractResponse",
),
},
},
},
"502": {
description:
"Upstream provider returned an empty, unparseable, or off-schema reply.",
content: { "application/json": { schema: errorEnvelope } },
},
"503": {
description:
"No AI provider configured for the calling user (or operator).",
content: { "application/json": { schema: errorEnvelope } },
},
...stdResponses,
},
},
},
"/api/medications/{id}/cadence": {
get: {
tags: ["Medications"],