From 820d58ea3791ce42a89373851b9716859f6d95b9 Mon Sep 17 00:00:00 2001 From: Piotr Oleszczyk Date: Wed, 4 Mar 2026 01:22:57 +0100 Subject: [PATCH] feat(routines): enrich single AI suggestions with concise context --- backend/innercontext/api/routines.py | 78 +++++++++++++++++-- frontend/messages/en.json | 4 + frontend/messages/pl.json | 4 + frontend/src/lib/types.ts | 9 +++ .../src/routes/routines/suggest/+page.svelte | 26 ++++++- 5 files changed, 113 insertions(+), 8 deletions(-) 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)}%

+ {#if suggestion.summary.constraints_applied?.length} +

+ {m["suggest_summaryConstraints"]()}: {suggestion.summary.constraints_applied.join(' · ')} +

+ {/if} +
+
+ {/if} +
{#each suggestion.steps as step, i (i)} @@ -217,10 +231,18 @@ {i + 1}
-

{stepLabel(step)}

+
+

{stepLabel(step)}

+ {#if step.optional} + {m["suggest_stepOptionalBadge"]()} + {/if} +
{#if stepMeta(step)}

{stepMeta(step)}

{/if} + {#if step.why_this_step} +

{step.why_this_step}

+ {/if}
{/each}