diff --git a/backend/innercontext/api/routines.py b/backend/innercontext/api/routines.py
index 4cac357..3292ca1 100644
--- a/backend/innercontext/api/routines.py
+++ b/backend/innercontext/api/routines.py
@@ -5,7 +5,7 @@ from uuid import UUID, uuid4
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel as PydanticBase
-from sqlmodel import Session, SQLModel, col, select
+from sqlmodel import Field, Session, SQLModel, col, select
from db import get_session
from innercontext.api.utils import get_or_404
@@ -76,6 +76,8 @@ class SuggestedStep(SQLModel):
action_notes: Optional[str] = None
dose: Optional[str] = None
region: Optional[str] = None
+ why_this_step: Optional[str] = None
+ optional: Optional[bool] = None
class SuggestRoutineRequest(SQLModel):
@@ -86,9 +88,16 @@ class SuggestRoutineRequest(SQLModel):
leaving_home: Optional[bool] = None
+class RoutineSuggestionSummary(SQLModel):
+ primary_goal: str = ""
+ constraints_applied: list[str] = Field(default_factory=list)
+ confidence: float = 0.0
+
+
class RoutineSuggestion(SQLModel):
steps: list[SuggestedStep]
reasoning: str
+ summary: Optional[RoutineSuggestionSummary] = None
class SuggestBatchRequest(SQLModel):
@@ -116,7 +125,17 @@ class BatchSuggestion(SQLModel):
# ---------------------------------------------------------------------------
-class _StepOut(PydanticBase):
+class _SingleStepOut(PydanticBase):
+ product_id: Optional[str] = None
+ action_type: Optional[GroomingAction] = None
+ dose: Optional[str] = None
+ region: Optional[str] = None
+ action_notes: Optional[str] = None
+ why_this_step: Optional[str] = None
+ optional: Optional[bool] = None
+
+
+class _BatchStepOut(PydanticBase):
product_id: Optional[str] = None
action_type: Optional[GroomingAction] = None
dose: Optional[str] = None
@@ -124,15 +143,22 @@ class _StepOut(PydanticBase):
action_notes: Optional[str] = None
+class _SummaryOut(PydanticBase):
+ primary_goal: str
+ constraints_applied: list[str]
+ confidence: float
+
+
class _SuggestionOut(PydanticBase):
- steps: list[_StepOut]
+ steps: list[_SingleStepOut]
reasoning: str
+ summary: _SummaryOut
class _DayPlanOut(PydanticBase):
date: str
- am_steps: list[_StepOut]
- pm_steps: list[_StepOut]
+ am_steps: list[_BatchStepOut]
+ pm_steps: list[_BatchStepOut]
reasoning: str
@@ -424,6 +450,8 @@ ZASADY PLANOWANIA:
- Jeśli krok to produkt: podaj poprawny UUID z listy.
- Jeśli krok to czynność pielęgnacyjna: product_id = null. Dozwolone akcje są ściśle określone w schemacie (action_type).
- Nie zwracaj "pustych" kroków: każdy krok musi mieć product_id albo action_type.
+- Pole region uzupełniaj tylko gdy ma znaczenie kliniczne/praktyczne (np. broda, wąsy, okolica oczu, szyja).
+ Dla standardowych kroków pielęgnacji całej twarzy pozostaw region puste.
JAK ROZWIĄZYWAĆ KONFLIKTY:
- Gdy cel użytkownika koliduje z bezpieczeństwem, wybierz bezpieczeństwo.
@@ -432,6 +460,17 @@ JAK ROZWIĄZYWAĆ KONFLIKTY:
"""
+_ROUTINES_SINGLE_EXTRA = """\
+DODATKOWE WYMAGANIA DLA TRYBU JEDNEJ RUTYNY:
+- Każdy krok powinien mieć zwięzłe why_this_step (maks. jedno zdanie).
+- Pole optional ustawiaj na true tylko dla kroków niekrytycznych.
+- Uzupełnij summary:
+ - primary_goal: główny cel tej rutyny,
+ - constraints_applied: lista kluczowych ograniczeń zastosowanych przy planowaniu,
+ - confidence: liczba 0-1.
+"""
+
+
# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------
@@ -515,6 +554,7 @@ def suggest_routine(
"INPUT DATA:\n"
f"{skin_ctx}\n{grooming_ctx}\n{history_ctx}\n{day_ctx}\n{products_ctx}\n{objectives_ctx}"
f"{notes_line}\n"
+ f"{_ROUTINES_SINGLE_EXTRA}\n"
"Zwróć JSON zgodny ze schematem."
)
@@ -545,10 +585,34 @@ def suggest_routine(
action_notes=s.get("action_notes"),
dose=s.get("dose"),
region=s.get("region"),
+ why_this_step=s.get("why_this_step"),
+ optional=s.get("optional"),
)
for s in parsed.get("steps", [])
]
- return RoutineSuggestion(steps=steps, reasoning=parsed.get("reasoning", ""))
+
+ summary_raw = parsed.get("summary") or {}
+ confidence_raw = summary_raw.get("confidence", 0)
+ try:
+ confidence = float(confidence_raw)
+ except (TypeError, ValueError):
+ confidence = 0.0
+ confidence = max(0.0, min(1.0, confidence))
+ constraints_applied = summary_raw.get("constraints_applied") or []
+ if not isinstance(constraints_applied, list):
+ constraints_applied = []
+
+ summary = RoutineSuggestionSummary(
+ primary_goal=str(summary_raw.get("primary_goal") or ""),
+ constraints_applied=[str(x) for x in constraints_applied],
+ confidence=confidence,
+ )
+
+ return RoutineSuggestion(
+ steps=steps,
+ reasoning=parsed.get("reasoning", ""),
+ summary=summary,
+ )
@router.post("/suggest-batch", response_model=BatchSuggestion)
@@ -624,6 +688,8 @@ def suggest_batch(
action_notes=s.get("action_notes"),
dose=s.get("dose"),
region=s.get("region"),
+ why_this_step=s.get("why_this_step"),
+ optional=s.get("optional"),
)
)
return result
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 4cc2be2..35d2c3a 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -171,6 +171,10 @@
"suggest_errorSave": "Error saving.",
"suggest_amMorning": "AM (morning)",
"suggest_pmEvening": "PM (evening)",
+ "suggest_summaryPrimaryGoal": "Primary goal",
+ "suggest_summaryConfidence": "Confidence",
+ "suggest_summaryConstraints": "Constraints",
+ "suggest_stepOptionalBadge": "optional",
"medications_title": "Medications",
"medications_count": "{count} entries",
diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json
index 597fb52..60524d2 100644
--- a/frontend/messages/pl.json
+++ b/frontend/messages/pl.json
@@ -171,6 +171,10 @@
"suggest_errorSave": "Błąd podczas zapisywania.",
"suggest_amMorning": "AM (rano)",
"suggest_pmEvening": "PM (wieczór)",
+ "suggest_summaryPrimaryGoal": "Główny cel",
+ "suggest_summaryConfidence": "Pewność",
+ "suggest_summaryConstraints": "Ograniczenia",
+ "suggest_stepOptionalBadge": "opcjonalny",
"medications_title": "Leki",
"medications_count": "{count} wpisów",
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts
index 876e31a..18d7fae 100644
--- a/frontend/src/lib/types.ts
+++ b/frontend/src/lib/types.ts
@@ -224,11 +224,20 @@ export interface SuggestedStep {
action_notes?: string;
dose?: string;
region?: string;
+ why_this_step?: string;
+ optional?: boolean;
+}
+
+export interface RoutineSuggestionSummary {
+ primary_goal: string;
+ constraints_applied: string[];
+ confidence: number;
}
export interface RoutineSuggestion {
steps: SuggestedStep[];
reasoning: string;
+ summary?: RoutineSuggestionSummary;
}
export interface DayPlan {
diff --git a/frontend/src/routes/routines/suggest/+page.svelte b/frontend/src/routes/routines/suggest/+page.svelte
index 8c295a3..e0de468 100644
--- a/frontend/src/routes/routines/suggest/+page.svelte
+++ b/frontend/src/routes/routines/suggest/+page.svelte
@@ -45,7 +45,7 @@
function stepMeta(step: SuggestedStep): string {
const parts: string[] = [];
if (step.dose) parts.push(step.dose);
- if (step.region) parts.push(step.region);
+ if (step.region && step.region.toLowerCase() !== 'face') parts.push(step.region);
if (step.action_notes && !step.action_type) parts.push(step.action_notes);
return parts.join(' · ');
}
@@ -209,6 +209,20 @@
+ {#if suggestion.summary}
+ {m["suggest_summaryPrimaryGoal"]()}: {suggestion.summary.primary_goal || '—'} {m["suggest_summaryConfidence"]()}: {Math.round((suggestion.summary.confidence ?? 0) * 100)}%
+ {m["suggest_summaryConstraints"]()}: {suggestion.summary.constraints_applied.join(' · ')}
+
{stepLabel(step)}
+{stepLabel(step)}
+ {#if step.optional} +{stepMeta(step)}
{/if} + {#if step.why_this_step} +{step.why_this_step}
+ {/if}