refactor(skin): replace trend with texture field on SkinConditionSnapshot
Remove the derived `trend` field (better computed from history by the MCP agent) and add `texture: smooth|rough|flaky|bumpy` which LLM can reliably assess from photos. Updates model, API, system prompt, tests, and frontend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
abf9593857
commit
4954d4f449
8 changed files with 30 additions and 31 deletions
|
|
@ -20,7 +20,7 @@ from innercontext.models.enums import (
|
||||||
BarrierState,
|
BarrierState,
|
||||||
OverallSkinState,
|
OverallSkinState,
|
||||||
SkinConcern,
|
SkinConcern,
|
||||||
SkinTrend,
|
SkinTexture,
|
||||||
SkinType,
|
SkinType,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -39,8 +39,8 @@ class SnapshotCreate(SkinConditionSnapshotBase):
|
||||||
class SnapshotUpdate(SQLModel):
|
class SnapshotUpdate(SQLModel):
|
||||||
snapshot_date: Optional[date] = None
|
snapshot_date: Optional[date] = None
|
||||||
overall_state: Optional[OverallSkinState] = None
|
overall_state: Optional[OverallSkinState] = None
|
||||||
trend: Optional[SkinTrend] = None
|
|
||||||
skin_type: Optional[SkinType] = None
|
skin_type: Optional[SkinType] = None
|
||||||
|
texture: Optional[SkinTexture] = None
|
||||||
|
|
||||||
hydration_level: Optional[int] = None
|
hydration_level: Optional[int] = None
|
||||||
sebum_tzone: Optional[int] = None
|
sebum_tzone: Optional[int] = None
|
||||||
|
|
@ -57,8 +57,8 @@ class SnapshotUpdate(SQLModel):
|
||||||
|
|
||||||
class SkinPhotoAnalysisResponse(SQLModel):
|
class SkinPhotoAnalysisResponse(SQLModel):
|
||||||
overall_state: Optional[OverallSkinState] = None
|
overall_state: Optional[OverallSkinState] = None
|
||||||
trend: Optional[SkinTrend] = None
|
|
||||||
skin_type: Optional[SkinType] = None
|
skin_type: Optional[SkinType] = None
|
||||||
|
texture: Optional[SkinTexture] = None
|
||||||
hydration_level: Optional[int] = None
|
hydration_level: Optional[int] = None
|
||||||
sebum_tzone: Optional[int] = None
|
sebum_tzone: Optional[int] = None
|
||||||
sebum_cheeks: Optional[int] = None
|
sebum_cheeks: Optional[int] = None
|
||||||
|
|
@ -85,14 +85,13 @@ RULES:
|
||||||
- Omit any field you cannot confidently determine from the photos. Do not guess.
|
- Omit any field you cannot confidently determine from the photos. Do not guess.
|
||||||
- All enum values must exactly match the allowed strings listed below.
|
- All enum values must exactly match the allowed strings listed below.
|
||||||
- Numeric metrics use a 1–5 scale (1 = minimal, 5 = maximal).
|
- Numeric metrics use a 1–5 scale (1 = minimal, 5 = maximal).
|
||||||
- For trends: only populate if you have strong visual cues; otherwise omit.
|
|
||||||
- risks and priorities: short English phrases, max 10 words each.
|
- risks and priorities: short English phrases, max 10 words each.
|
||||||
- notes: 2–4 sentence paragraph describing key observations.
|
- notes: 2–4 sentence paragraph describing key observations.
|
||||||
|
|
||||||
ENUM VALUES:
|
ENUM VALUES:
|
||||||
overall_state: "excellent" | "good" | "fair" | "poor"
|
overall_state: "excellent" | "good" | "fair" | "poor"
|
||||||
trend: "improving" | "stable" | "worsening" | "fluctuating"
|
|
||||||
skin_type: "dry" | "oily" | "combination" | "sensitive" | "normal" | "acne_prone"
|
skin_type: "dry" | "oily" | "combination" | "sensitive" | "normal" | "acne_prone"
|
||||||
|
texture: "smooth" | "rough" | "flaky" | "bumpy"
|
||||||
barrier_state: "intact" | "mildly_compromised" | "compromised"
|
barrier_state: "intact" | "mildly_compromised" | "compromised"
|
||||||
active_concerns: "acne" | "rosacea" | "hyperpigmentation" | "aging" | "dehydration" |
|
active_concerns: "acne" | "rosacea" | "hyperpigmentation" | "aging" | "dehydration" |
|
||||||
"redness" | "damaged_barrier" | "pore_visibility" | "uneven_texture" | "sebum_excess"
|
"redness" | "damaged_barrier" | "pore_visibility" | "uneven_texture" | "sebum_excess"
|
||||||
|
|
@ -104,7 +103,7 @@ sebum_cheeks: 1=very dry cheeks → 5=very oily cheeks
|
||||||
sensitivity_level: 1=no visible signs → 5=severe redness/reactivity
|
sensitivity_level: 1=no visible signs → 5=severe redness/reactivity
|
||||||
|
|
||||||
OUTPUT (all fields optional):
|
OUTPUT (all fields optional):
|
||||||
{"overall_state":…, "trend":…, "skin_type":…, "hydration_level":…,
|
{"overall_state":…, "skin_type":…, "texture":…, "hydration_level":…,
|
||||||
"sebum_tzone":…, "sebum_cheeks":…, "sensitivity_level":…,
|
"sebum_tzone":…, "sebum_cheeks":…, "sensitivity_level":…,
|
||||||
"barrier_state":…, "active_concerns":[…], "risks":[…], "priorities":[…], "notes":…}
|
"barrier_state":…, "active_concerns":[…], "risks":[…], "priorities":[…], "notes":…}
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from .enums import (
|
||||||
ResultFlag,
|
ResultFlag,
|
||||||
RoutineRole,
|
RoutineRole,
|
||||||
SkinConcern,
|
SkinConcern,
|
||||||
SkinTrend,
|
SkinTexture,
|
||||||
SkinType,
|
SkinType,
|
||||||
StrengthLevel,
|
StrengthLevel,
|
||||||
TextureType,
|
TextureType,
|
||||||
|
|
@ -59,7 +59,7 @@ __all__ = [
|
||||||
"ResultFlag",
|
"ResultFlag",
|
||||||
"RoutineRole",
|
"RoutineRole",
|
||||||
"SkinConcern",
|
"SkinConcern",
|
||||||
"SkinTrend",
|
"SkinTexture",
|
||||||
"SkinType",
|
"SkinType",
|
||||||
"StrengthLevel",
|
"StrengthLevel",
|
||||||
"TextureType",
|
"TextureType",
|
||||||
|
|
|
||||||
|
|
@ -186,11 +186,11 @@ class OverallSkinState(str, Enum):
|
||||||
POOR = "poor"
|
POOR = "poor"
|
||||||
|
|
||||||
|
|
||||||
class SkinTrend(str, Enum):
|
class SkinTexture(str, Enum):
|
||||||
IMPROVING = "improving"
|
SMOOTH = "smooth"
|
||||||
STABLE = "stable"
|
ROUGH = "rough"
|
||||||
WORSENING = "worsening"
|
FLAKY = "flaky"
|
||||||
FLUCTUATING = "fluctuating"
|
BUMPY = "bumpy"
|
||||||
|
|
||||||
|
|
||||||
class BarrierState(str, Enum):
|
class BarrierState(str, Enum):
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
from .base import utc_now
|
from .base import utc_now
|
||||||
from .domain import Domain
|
from .domain import Domain
|
||||||
from .enums import BarrierState, OverallSkinState, SkinConcern, SkinTrend, SkinType
|
from .enums import BarrierState, OverallSkinState, SkinConcern, SkinTexture, SkinType
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Base model (pure Python types, no sa_column, no id/created_at)
|
# Base model (pure Python types, no sa_column, no id/created_at)
|
||||||
|
|
@ -20,8 +20,8 @@ class SkinConditionSnapshotBase(SQLModel):
|
||||||
snapshot_date: date
|
snapshot_date: date
|
||||||
|
|
||||||
overall_state: OverallSkinState | None = None
|
overall_state: OverallSkinState | None = None
|
||||||
trend: SkinTrend | None = None
|
|
||||||
skin_type: SkinType | None = None
|
skin_type: SkinType | None = None
|
||||||
|
texture: SkinTexture | None = None
|
||||||
|
|
||||||
# Metryki wizualne (1 = minimalne, 5 = maksymalne nasilenie)
|
# Metryki wizualne (1 = minimalne, 5 = maksymalne nasilenie)
|
||||||
hydration_level: int | None = Field(default=None, ge=1, le=5)
|
hydration_level: int | None = Field(default=None, ge=1, le=5)
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ def test_create_snapshot_full(client):
|
||||||
json={
|
json={
|
||||||
"snapshot_date": "2026-02-20",
|
"snapshot_date": "2026-02-20",
|
||||||
"overall_state": "good",
|
"overall_state": "good",
|
||||||
"trend": "improving",
|
|
||||||
"skin_type": "combination",
|
"skin_type": "combination",
|
||||||
|
"texture": "rough",
|
||||||
"hydration_level": 3,
|
"hydration_level": 3,
|
||||||
"sebum_tzone": 4,
|
"sebum_tzone": 4,
|
||||||
"sebum_cheeks": 2,
|
"sebum_cheeks": 2,
|
||||||
|
|
@ -32,7 +32,7 @@ def test_create_snapshot_full(client):
|
||||||
assert r.status_code == 201
|
assert r.status_code == 201
|
||||||
data = r.json()
|
data = r.json()
|
||||||
assert data["overall_state"] == "good"
|
assert data["overall_state"] == "good"
|
||||||
assert data["trend"] == "improving"
|
assert data["texture"] == "rough"
|
||||||
assert "acne" in data["active_concerns"]
|
assert "acne" in data["active_concerns"]
|
||||||
assert data["hydration_level"] == 3
|
assert data["hydration_level"] == 3
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export type SkinConcern =
|
||||||
| 'uneven_texture'
|
| 'uneven_texture'
|
||||||
| 'hair_growth'
|
| 'hair_growth'
|
||||||
| 'sebum_excess';
|
| 'sebum_excess';
|
||||||
export type SkinTrend = 'improving' | 'stable' | 'worsening' | 'fluctuating';
|
export type SkinTexture = 'smooth' | 'rough' | 'flaky' | 'bumpy';
|
||||||
export type SkinType = 'dry' | 'oily' | 'combination' | 'sensitive' | 'normal' | 'acne_prone';
|
export type SkinType = 'dry' | 'oily' | 'combination' | 'sensitive' | 'normal' | 'acne_prone';
|
||||||
export type StrengthLevel = 1 | 2 | 3;
|
export type StrengthLevel = 1 | 2 | 3;
|
||||||
export type TextureType = 'watery' | 'gel' | 'emulsion' | 'cream' | 'oil' | 'balm' | 'foam' | 'fluid';
|
export type TextureType = 'watery' | 'gel' | 'emulsion' | 'cream' | 'oil' | 'balm' | 'foam' | 'fluid';
|
||||||
|
|
@ -250,8 +250,8 @@ export interface SkinConditionSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
snapshot_date: string;
|
snapshot_date: string;
|
||||||
overall_state?: OverallSkinState;
|
overall_state?: OverallSkinState;
|
||||||
trend?: SkinTrend;
|
|
||||||
skin_type?: SkinType;
|
skin_type?: SkinType;
|
||||||
|
texture?: SkinTexture;
|
||||||
hydration_level?: number;
|
hydration_level?: number;
|
||||||
sebum_tzone?: number;
|
sebum_tzone?: number;
|
||||||
sebum_cheeks?: number;
|
sebum_cheeks?: number;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export const actions: Actions = {
|
||||||
const form = await request.formData();
|
const form = await request.formData();
|
||||||
const snapshot_date = form.get('snapshot_date') as string;
|
const snapshot_date = form.get('snapshot_date') as string;
|
||||||
const overall_state = form.get('overall_state') as string;
|
const overall_state = form.get('overall_state') as string;
|
||||||
const trend = form.get('trend') as string;
|
const texture = form.get('texture') as string;
|
||||||
const notes = form.get('notes') as string;
|
const notes = form.get('notes') as string;
|
||||||
const hydration_level = form.get('hydration_level') as string;
|
const hydration_level = form.get('hydration_level') as string;
|
||||||
const sensitivity_level = form.get('sensitivity_level') as string;
|
const sensitivity_level = form.get('sensitivity_level') as string;
|
||||||
|
|
@ -35,7 +35,7 @@ export const actions: Actions = {
|
||||||
|
|
||||||
const body: Record<string, unknown> = { snapshot_date, active_concerns };
|
const body: Record<string, unknown> = { snapshot_date, active_concerns };
|
||||||
if (overall_state) body.overall_state = overall_state;
|
if (overall_state) body.overall_state = overall_state;
|
||||||
if (trend) body.trend = trend;
|
if (texture) body.texture = texture;
|
||||||
if (notes) body.notes = notes;
|
if (notes) body.notes = notes;
|
||||||
if (hydration_level) body.hydration_level = Number(hydration_level);
|
if (hydration_level) body.hydration_level = Number(hydration_level);
|
||||||
if (sensitivity_level) body.sensitivity_level = Number(sensitivity_level);
|
if (sensitivity_level) body.sensitivity_level = Number(sensitivity_level);
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||||
|
|
||||||
const states = ['excellent', 'good', 'fair', 'poor'];
|
const states = ['excellent', 'good', 'fair', 'poor'];
|
||||||
const trends = ['improving', 'stable', 'worsening', 'fluctuating'];
|
const skinTextures = ['smooth', 'rough', 'flaky', 'bumpy'];
|
||||||
const barrierStates = ['intact', 'mildly_compromised', 'compromised'];
|
const barrierStates = ['intact', 'mildly_compromised', 'compromised'];
|
||||||
const skinTypes = ['dry', 'oily', 'combination', 'sensitive', 'normal', 'acne_prone'];
|
const skinTypes = ['dry', 'oily', 'combination', 'sensitive', 'normal', 'acne_prone'];
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
// Form state (bound to inputs so AI can pre-fill)
|
// Form state (bound to inputs so AI can pre-fill)
|
||||||
let snapshotDate = $state(new Date().toISOString().slice(0, 10));
|
let snapshotDate = $state(new Date().toISOString().slice(0, 10));
|
||||||
let overallState = $state('');
|
let overallState = $state('');
|
||||||
let trend = $state('');
|
let texture = $state('');
|
||||||
let barrierState = $state('');
|
let barrierState = $state('');
|
||||||
let skinType = $state('');
|
let skinType = $state('');
|
||||||
let hydrationLevel = $state('');
|
let hydrationLevel = $state('');
|
||||||
|
|
@ -64,7 +64,7 @@
|
||||||
try {
|
try {
|
||||||
const r = await analyzeSkinPhotos(selectedFiles);
|
const r = await analyzeSkinPhotos(selectedFiles);
|
||||||
if (r.overall_state) overallState = r.overall_state;
|
if (r.overall_state) overallState = r.overall_state;
|
||||||
if (r.trend) trend = r.trend;
|
if (r.texture) texture = r.texture;
|
||||||
if (r.skin_type) skinType = r.skin_type;
|
if (r.skin_type) skinType = r.skin_type;
|
||||||
if (r.barrier_state) barrierState = r.barrier_state;
|
if (r.barrier_state) barrierState = r.barrier_state;
|
||||||
if (r.hydration_level != null) hydrationLevel = String(r.hydration_level);
|
if (r.hydration_level != null) hydrationLevel = String(r.hydration_level);
|
||||||
|
|
@ -178,12 +178,12 @@
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<Label>Trend</Label>
|
<Label>Texture</Label>
|
||||||
<input type="hidden" name="trend" value={trend} />
|
<input type="hidden" name="texture" value={texture} />
|
||||||
<Select type="single" value={trend} onValueChange={(v) => (trend = v)}>
|
<Select type="single" value={texture} onValueChange={(v) => (texture = v)}>
|
||||||
<SelectTrigger>{trend || 'Select'}</SelectTrigger>
|
<SelectTrigger>{texture || 'Select'}</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{#each trends as t (t)}
|
{#each skinTextures as t (t)}
|
||||||
<SelectItem value={t}>{t}</SelectItem>
|
<SelectItem value={t}>{t}</SelectItem>
|
||||||
{/each}
|
{/each}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -296,8 +296,8 @@
|
||||||
{snap.overall_state}
|
{snap.overall_state}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if snap.trend}
|
{#if snap.texture}
|
||||||
<Badge variant="secondary">{snap.trend}</Badge>
|
<Badge variant="secondary">{snap.texture}</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue