Introduces `get_extraction_config` and `get_creative_config` to standardize Gemini API calls.
* Defines explicit config profiles with appropriate `temperature` and `thinking_level` for Gemini 3 Flash.
* Extraction tasks use minimal thinking and temp=0.0 to reduce latency and token usage.
* Creative tasks use low thinking, temp=0.4, and top_p=0.8 to balance naturalness and safety.
* Applies these helpers across products, routines, and skincare endpoints.
* Also updates default model to `gemini-3-flash-preview`.
- Add POST /api/products/suggest endpoint that analyzes skin condition
and inventory to suggest product types (e.g., 'Salicylic Acid 2% Masque')
- Add MCP tool get_shopping_suggestions() for MCP clients
- Add 'Suggest' button to Products page in frontend
- Add /products/suggest page with suggestion cards
- Include product type, key ingredients, target concerns, why_needed,
recommended_time, and frequency in suggestions
- Fix stock logic: sealed products now count as available inventory
- Add legend to clarify ✓ (in stock) vs ✗ (not in stock) markers
- Remove _build_inventory_context; fold pao_months into DOSTĘPNE PRODUKTY entries
- Remove "Otwarte równolegle" duplicate section from prompt
- Rename OSTATNIE RUTYNY (7 dni) → OSTATNIE RUTYNY
- Add _build_day_context and SuggestRoutineRequest.leaving_home (optional bool)
- System prompt: replace unconditional PAO rule with conditional; add SPF factor
selection logic based on KONTEKST DNIA leaving_home value
- Frontend: leaving_home checkbox (AM only) + i18n keys pl/en
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gemini API rejects int-valued enums (StrengthLevel) in response_schema,
raising a validation error before any request is sent. Fix by introducing
AIActiveIngredient (inherits ActiveIngredient, overrides strength_level and
irritation_potential as Optional[int]) and ProductParseLLMResponse used only
as the Gemini schema. The two-step validation converts ints back to StrengthLevel
via Pydantic coercion. Adds a test covering the numeric strength level path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gemini-flash-latest resolves to gemini-3-flash-preview which uses
thinking_level instead of the legacy thinking_budget (mixing both
returns HTTP 400). Use LOW to reduce thinking overhead while keeping
basic reasoning, replacing the now-incompatible thinking_budget=0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Gemini stops generation early (e.g. due to safety filters or
thinking-model quirks), finish_reason != STOP but no exception is raised,
causing the caller to receive truncated JSON and a confusing 502 "invalid
JSON" error. Now:
- finish_reason is extracted from candidates[0] and stored in ai_call_logs
- any non-STOP finish_reason raises HTTP 502 with a clear message
- Alembic migration adds the finish_reason column to ai_call_logs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add include_minoxidil_beard flag to SuggestRoutineRequest and SuggestBatchRequest
- Detect minoxidil products by scanning name, brand, INCI and actives; pass them
to the LLM even though they are medications
- Inject CELE UŻYTKOWNIKA context block into prompts when flag is enabled
- Add _build_objectives_context() returning empty string when flag is off
- Add call_gemini() helper that centralises Gemini API calls and logs every
request/response to a new ai_call_logs table (AICallLog model + /ai-logs router)
- Nginx: raise client_max_body_size to 16 MB for photo uploads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Batch-load all routine steps in a single query and attach them to each
routine dict, mirroring the detail endpoint pattern. Fixes "0 steps"
shown on the routines list page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without cascade, SQLAlchemy tried to NULL-out foreign keys on child rows
before deleting the parent, hitting NOT NULL constraints in PostgreSQL.
- Routine.steps: cascade="all, delete-orphan" (routine_steps.routine_id)
- MedicationEntry.usage_history: cascade="all, delete-orphan"
(medication_usages.medication_record_id)
Product.inventory already had cascade set correctly.
No DB migration needed — ORM-level only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass response_schema to all three generate_content calls so Gemini
constrains its output to valid enum values and correct JSON structure:
- routines.py: _StepOut.action_type Optional[str] → Optional[GroomingAction]
- skincare.py: add _SkinAnalysisOut(PydanticBase) with OverallSkinState,
SkinType, SkinTexture, BarrierState, SkinConcern enums; add response_schema
- products.py: pass ProductParseResponse directly as response_schema;
remove NaN/Infinity/undefined regex cleanup, markdown-fence extraction,
finish_reason logging, and re import — all now unnecessary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Gemini-powered endpoints and frontend pages for proposing skincare
routines based on skin state, product compatibility, grooming schedule,
and recent history.
Backend (routines.py):
- POST /routines/suggest — single AM/PM routine for a date
- POST /routines/suggest-batch — AM+PM plan for up to 14 days
- Prompt context: skin snapshot, grooming schedule, 7-day history,
filtered product list with effects/incompatibilities/context rules
- Respects retinoid frequency limits, acid/retinoid separation,
grooming-aware safe_after_shaving rules
Frontend:
- /routines/suggest page with tab switcher (single / batch)
- Single tab: date + AM/PM + optional notes → generate → preview → save
- Batch tab: date range + notes → collapsible day cards (AM+PM) → save all
- Loading spinner during Gemini calls; product names resolved from map
- "Zaproponuj rutynę AI" button added to routines list page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace category filter dropdown with client-side grouping and a
3-way ownership toggle (All / Owned / Not owned). Products are grouped
by category with header rows as visual dividers, sorted brand → name
within each group. Category column removed (redundant with headings).
Backend: GET /products now returns ProductWithInventory so inventory
data is available for ownership filtering (bulk-loaded in one query).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SQLAlchemy was nulling out product_id on ProductInventory rows instead
of deleting them. Added cascade="all, delete-orphan" to the ORM
relationship and ondelete="CASCADE" to the FK field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Model was emitting "anti_aging" as a valid ingredient function
(e.g. for retinoids, peptides). Add it to the enum and the
parse-text system prompt allowed values.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regular generation was hitting MAX_TOKENS at 8192. Constrained decoding with
16384 should be a viable middle ground between the truncation at 8192 and the
timeout at 65536.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Constrained decoding is ~10x slower and consumes hidden tokens for constraint
processing, causing truncation at ~1000 chars even with 8192 max_output_tokens.
The system prompt already instructs the model to output raw minified JSON; our
NaN/markdown-fence sanitisation handles edge cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace truncation-recovery heuristic with a higher token budget.
On JSON parse failure, log finish_reason and 160-char error context
to make the root cause visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pretty-printed JSON wastes 2-3x tokens on indentation/newlines.
Minified output fits more data (e.g. long INCI lists) within the
8192 output token limit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Models sometimes emit JS-style literals for unknown numeric fields.
Replace NaN, Infinity, undefined with null before parsing.
Also add error logging to capture raw response on parse failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Increase max_output_tokens 4096 → 8192 to prevent truncated JSON on
products with long INCI lists
- Return explicit 502 when response.text is None (safety filter blocks)
- Fallback JSON extraction strips markdown fences or leading preamble
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add alembic 1.14 to dependencies (uv sync → 1.18.4 installed)
- Configure alembic/env.py: loads DATABASE_URL from env, imports all
SQLModel models so metadata is fully populated for autogenerate
- Generate initial migration (c2d626a2b36c) covering all 9 tables:
products, product_inventory, medication_entries, medication_usages,
lab_results, routines, routine_steps, grooming_schedule,
skin_condition_snapshots — with all indexes and constraints
- Add ExecStartPre to innercontext.service: runs alembic upgrade head
before uvicorn starts (idempotent, safe on every restart)
- Update DEPLOYMENT.md: add migration step to backend setup and update
flow; document alembic stamp head for existing installations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add backend/innercontext/mcp_server.py with tools covering products,
inventory, routines, skin snapshots, medications, lab results, and
grooming schedule
- Mount MCP app at /mcp in main.py using combine_lifespans
- Fix test isolation: patch app.router.lifespan_context in conftest to
avoid StreamableHTTPSessionManager single-run limitation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Pass `text=` as keyword arg to Part.from_text() and raise max_output_tokens
from 1024 to 2048 to prevent JSON truncation in the notes field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add POST /skincare/analyze-photos endpoint that accepts 1–3 skin
photos, sends them to Gemini vision, and returns a structured
SkinPhotoAnalysisResponse for pre-filling the snapshot form.
Extract shared Gemini client setup into innercontext/llm.py
(get_gemini_client) so both products and skincare use a single
default model (gemini-flash-latest) and API key check.
Frontend: AI photo card on /skin page with file picker, previews,
and auto-fill of all form fields from the analysis result.
New fields (skin_type, sebum_tzone, sebum_cheeks) added to form
and server action.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add POST /products/parse-text endpoint that accepts raw product text,
calls Gemini (google-genai) with a structured extraction prompt, and
returns a partial ProductParseResponse. Frontend gains a collapsible
"AI pre-fill" card at the top of ProductForm that merges the LLM
response into all form fields reactively.
- Backend: ProductParseRequest/Response schemas, system prompt with
enum constraints, temperature=0.0 for deterministic extraction,
effect_profile always returned in full
- Frontend: parseProductText() in api.ts; controlled $state bindings
for all text/number/checkbox inputs; applyAiResult() merges response
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds full_weight_g and empty_weight_g to ProductBase (inherited by Product
and response models) so per-product package weight specs are captured.
Adds last_weighed_at to ProductInventory to record when a package was last
weighed. Wires up all fields through API schemas, frontend types, forms, and
the product detail page (add/edit/display).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add ProductBase, ProductPublic, ProductWithInventory and
SkinConditionSnapshotBase, SkinConditionSnapshotPublic. Table models now inherit
from their Base counterpart and override JSON fields with sa_column. All
field_serializer hacks removed; FastAPI response models use the non-table Public
classes so Pydantic coerces raw DB dicts → typed models cleanly. ProductCreate
and SnapshotCreate now simply inherit their respective Base classes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Drop Product.personal_rating from model, API schemas, and all frontend
views (list table, detail view, quick-edit form, new-product form)
- Extract get_or_404 into backend/innercontext/api/utils.py; remove five
duplicate copies from individual API modules
- Fix all ty type errors: generic get_or_404 with TypeVar, cast() in
coerce_effect_profile validator, col() for ilike on SQLModel column,
dict[str, Any] annotation in test helper, ty: ignore for CORSMiddleware
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>