feat(routines): enrich single AI suggestions with concise context
This commit is contained in:
parent
083cd055fb
commit
820d58ea37
5 changed files with 113 additions and 8 deletions
|
|
@ -5,7 +5,7 @@ from uuid import UUID, uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from pydantic import BaseModel as PydanticBase
|
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 db import get_session
|
||||||
from innercontext.api.utils import get_or_404
|
from innercontext.api.utils import get_or_404
|
||||||
|
|
@ -76,6 +76,8 @@ class SuggestedStep(SQLModel):
|
||||||
action_notes: Optional[str] = None
|
action_notes: Optional[str] = None
|
||||||
dose: Optional[str] = None
|
dose: Optional[str] = None
|
||||||
region: Optional[str] = None
|
region: Optional[str] = None
|
||||||
|
why_this_step: Optional[str] = None
|
||||||
|
optional: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
class SuggestRoutineRequest(SQLModel):
|
class SuggestRoutineRequest(SQLModel):
|
||||||
|
|
@ -86,9 +88,16 @@ class SuggestRoutineRequest(SQLModel):
|
||||||
leaving_home: Optional[bool] = None
|
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):
|
class RoutineSuggestion(SQLModel):
|
||||||
steps: list[SuggestedStep]
|
steps: list[SuggestedStep]
|
||||||
reasoning: str
|
reasoning: str
|
||||||
|
summary: Optional[RoutineSuggestionSummary] = None
|
||||||
|
|
||||||
|
|
||||||
class SuggestBatchRequest(SQLModel):
|
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
|
product_id: Optional[str] = None
|
||||||
action_type: Optional[GroomingAction] = None
|
action_type: Optional[GroomingAction] = None
|
||||||
dose: Optional[str] = None
|
dose: Optional[str] = None
|
||||||
|
|
@ -124,15 +143,22 @@ class _StepOut(PydanticBase):
|
||||||
action_notes: Optional[str] = None
|
action_notes: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class _SummaryOut(PydanticBase):
|
||||||
|
primary_goal: str
|
||||||
|
constraints_applied: list[str]
|
||||||
|
confidence: float
|
||||||
|
|
||||||
|
|
||||||
class _SuggestionOut(PydanticBase):
|
class _SuggestionOut(PydanticBase):
|
||||||
steps: list[_StepOut]
|
steps: list[_SingleStepOut]
|
||||||
reasoning: str
|
reasoning: str
|
||||||
|
summary: _SummaryOut
|
||||||
|
|
||||||
|
|
||||||
class _DayPlanOut(PydanticBase):
|
class _DayPlanOut(PydanticBase):
|
||||||
date: str
|
date: str
|
||||||
am_steps: list[_StepOut]
|
am_steps: list[_BatchStepOut]
|
||||||
pm_steps: list[_StepOut]
|
pm_steps: list[_BatchStepOut]
|
||||||
reasoning: str
|
reasoning: str
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -424,6 +450,8 @@ ZASADY PLANOWANIA:
|
||||||
- Jeśli krok to produkt: podaj poprawny UUID z listy.
|
- 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).
|
- 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.
|
- 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:
|
JAK ROZWIĄZYWAĆ KONFLIKTY:
|
||||||
- Gdy cel użytkownika koliduje z bezpieczeństwem, wybierz bezpieczeństwo.
|
- 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
|
# Helper
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -515,6 +554,7 @@ def suggest_routine(
|
||||||
"INPUT DATA:\n"
|
"INPUT DATA:\n"
|
||||||
f"{skin_ctx}\n{grooming_ctx}\n{history_ctx}\n{day_ctx}\n{products_ctx}\n{objectives_ctx}"
|
f"{skin_ctx}\n{grooming_ctx}\n{history_ctx}\n{day_ctx}\n{products_ctx}\n{objectives_ctx}"
|
||||||
f"{notes_line}\n"
|
f"{notes_line}\n"
|
||||||
|
f"{_ROUTINES_SINGLE_EXTRA}\n"
|
||||||
"Zwróć JSON zgodny ze schematem."
|
"Zwróć JSON zgodny ze schematem."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -545,10 +585,34 @@ def suggest_routine(
|
||||||
action_notes=s.get("action_notes"),
|
action_notes=s.get("action_notes"),
|
||||||
dose=s.get("dose"),
|
dose=s.get("dose"),
|
||||||
region=s.get("region"),
|
region=s.get("region"),
|
||||||
|
why_this_step=s.get("why_this_step"),
|
||||||
|
optional=s.get("optional"),
|
||||||
)
|
)
|
||||||
for s in parsed.get("steps", [])
|
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)
|
@router.post("/suggest-batch", response_model=BatchSuggestion)
|
||||||
|
|
@ -624,6 +688,8 @@ def suggest_batch(
|
||||||
action_notes=s.get("action_notes"),
|
action_notes=s.get("action_notes"),
|
||||||
dose=s.get("dose"),
|
dose=s.get("dose"),
|
||||||
region=s.get("region"),
|
region=s.get("region"),
|
||||||
|
why_this_step=s.get("why_this_step"),
|
||||||
|
optional=s.get("optional"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,10 @@
|
||||||
"suggest_errorSave": "Error saving.",
|
"suggest_errorSave": "Error saving.",
|
||||||
"suggest_amMorning": "AM (morning)",
|
"suggest_amMorning": "AM (morning)",
|
||||||
"suggest_pmEvening": "PM (evening)",
|
"suggest_pmEvening": "PM (evening)",
|
||||||
|
"suggest_summaryPrimaryGoal": "Primary goal",
|
||||||
|
"suggest_summaryConfidence": "Confidence",
|
||||||
|
"suggest_summaryConstraints": "Constraints",
|
||||||
|
"suggest_stepOptionalBadge": "optional",
|
||||||
|
|
||||||
"medications_title": "Medications",
|
"medications_title": "Medications",
|
||||||
"medications_count": "{count} entries",
|
"medications_count": "{count} entries",
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,10 @@
|
||||||
"suggest_errorSave": "Błąd podczas zapisywania.",
|
"suggest_errorSave": "Błąd podczas zapisywania.",
|
||||||
"suggest_amMorning": "AM (rano)",
|
"suggest_amMorning": "AM (rano)",
|
||||||
"suggest_pmEvening": "PM (wieczór)",
|
"suggest_pmEvening": "PM (wieczór)",
|
||||||
|
"suggest_summaryPrimaryGoal": "Główny cel",
|
||||||
|
"suggest_summaryConfidence": "Pewność",
|
||||||
|
"suggest_summaryConstraints": "Ograniczenia",
|
||||||
|
"suggest_stepOptionalBadge": "opcjonalny",
|
||||||
|
|
||||||
"medications_title": "Leki",
|
"medications_title": "Leki",
|
||||||
"medications_count": "{count} wpisów",
|
"medications_count": "{count} wpisów",
|
||||||
|
|
|
||||||
|
|
@ -224,11 +224,20 @@ export interface SuggestedStep {
|
||||||
action_notes?: string;
|
action_notes?: string;
|
||||||
dose?: string;
|
dose?: string;
|
||||||
region?: string;
|
region?: string;
|
||||||
|
why_this_step?: string;
|
||||||
|
optional?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoutineSuggestionSummary {
|
||||||
|
primary_goal: string;
|
||||||
|
constraints_applied: string[];
|
||||||
|
confidence: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RoutineSuggestion {
|
export interface RoutineSuggestion {
|
||||||
steps: SuggestedStep[];
|
steps: SuggestedStep[];
|
||||||
reasoning: string;
|
reasoning: string;
|
||||||
|
summary?: RoutineSuggestionSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DayPlan {
|
export interface DayPlan {
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
function stepMeta(step: SuggestedStep): string {
|
function stepMeta(step: SuggestedStep): string {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
if (step.dose) parts.push(step.dose);
|
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);
|
if (step.action_notes && !step.action_type) parts.push(step.action_notes);
|
||||||
return parts.join(' · ');
|
return parts.join(' · ');
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +209,20 @@
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{#if suggestion.summary}
|
||||||
|
<Card class="border-muted bg-muted/30">
|
||||||
|
<CardContent class="space-y-2 pt-4">
|
||||||
|
<p class="text-sm"><span class="font-medium">{m["suggest_summaryPrimaryGoal"]()}:</span> {suggestion.summary.primary_goal || '—'}</p>
|
||||||
|
<p class="text-sm"><span class="font-medium">{m["suggest_summaryConfidence"]()}:</span> {Math.round((suggestion.summary.confidence ?? 0) * 100)}%</p>
|
||||||
|
{#if suggestion.summary.constraints_applied?.length}
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{m["suggest_summaryConstraints"]()}: {suggestion.summary.constraints_applied.join(' · ')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Steps -->
|
<!-- Steps -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each suggestion.steps as step, i (i)}
|
{#each suggestion.steps as step, i (i)}
|
||||||
|
|
@ -217,10 +231,18 @@
|
||||||
{i + 1}
|
{i + 1}
|
||||||
</span>
|
</span>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<p class="text-sm font-medium">{stepLabel(step)}</p>
|
<p class="text-sm font-medium">{stepLabel(step)}</p>
|
||||||
|
{#if step.optional}
|
||||||
|
<Badge variant="secondary">{m["suggest_stepOptionalBadge"]()}</Badge>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{#if stepMeta(step)}
|
{#if stepMeta(step)}
|
||||||
<p class="text-xs text-muted-foreground">{stepMeta(step)}</p>
|
<p class="text-xs text-muted-foreground">{stepMeta(step)}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if step.why_this_step}
|
||||||
|
<p class="text-xs text-muted-foreground italic">{step.why_this_step}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue