Commit graph

55 commits

Author SHA1 Message Date
5dd8242985 fix(routines): simplify inventory preference in system prompt 2026-03-04 12:18:07 +01:00
b58fcb1440 feat(api): add tool-calling flow for shopping suggestions
Keep /products/suggest lean by exposing product UUIDs and fetching INCI, safety rules, actives, and usage notes on demand through Gemini function tools. Add conservative fallback behavior for tool roundtrip limits and expand helper tests to cover tool wiring and payload handlers.
2026-03-04 12:05:33 +01:00
558708653c feat(api): expand routines tool-calling to reduce prompt load
Keep the /routines/suggest base context lean by sending only active names and fetching detailed safety, actives, usage notes, and INCI on demand. Add a conservative fallback when tool roundtrip limits are hit to preserve safe outputs instead of failing the request.
2026-03-04 11:52:07 +01:00
cfd2485b7e feat(api): add INCI tool-calling with normalized tool traces
Enable on-demand INCI retrieval in /routines/suggest through Gemini function calling so detailed ingredient data is fetched only when needed. Persist and normalize tool_trace data in AI logs to make function-call behavior directly inspectable via /ai-logs endpoints.
2026-03-04 11:35:19 +01:00
c0eeb0425d fix(routines): include product safety and usage signals in prompts
Expose leave-on behavior, contraindications, safety alerts, and compact usage notes in AVAILABLE PRODUCTS so Gemini can make safer routine decisions with real-world product constraints.
2026-03-04 02:42:16 +01:00
472a3034a0 feat(routines): refine therapeutic and travel-mode prompt rules 2026-03-04 02:22:39 +01:00
820d58ea37 feat(routines): enrich single AI suggestions with concise context 2026-03-04 01:22:57 +01:00
88f3642387 test(api): add tests for ai suggestion endpoints and helpers 2026-03-03 22:06:33 +01:00
ba1f10d99f refactor(llm): optimize Gemini config profiles for extraction and creativity
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`.
2026-03-03 21:24:23 +01:00
78df7322a9 refactor(api): remove shopping assistant logic from mcp_server 2026-03-03 20:51:42 +01:00
0e7a39836f refactor(routines): use category and short uuid for recent history representation 2026-03-03 20:29:36 +01:00
28fb74b9bf refactor(routines): translate prompt input keys to english to reduce language switch penalty 2026-03-03 20:24:56 +01:00
9574c91be1 refactor(routines): remove hardcoded grooming actions from system prompt 2026-03-03 20:22:59 +01:00
4627ec70bf refactor(routines): remove examples from inventory management rule to avoid bias 2026-03-03 20:07:13 +01:00
30ebc093bf feat(routines): adjust inventory management prompt to allow opening better suited sealed products 2026-03-03 20:06:38 +01:00
877051cfaf feat(routines): add actives and recent usage tracking to product context 2026-03-03 20:01:39 +01:00
1109d9f397 fix(products): only suggest when real need exists 2026-03-03 19:51:49 +01:00
609995732b feat(routines): add minimize_products option for batch suggestions 2026-03-03 00:50:49 +01:00
40f9a353bb feat(products): add shopping suggestions feature
- 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
2026-03-02 22:38:08 +01:00
389ca5ffdc fix(backend): resolve ty check errors across api, mcp, and lifespan typing 2026-03-02 15:51:14 +01:00
c85ca355df refactor(routines): streamline suggest prompt — merge inventory context, add leaving_home SPF hint
- 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>
2026-03-01 23:47:54 +01:00
258b8c4330 refactor(routines): use SQLAlchemy is_(False) for product filters 2026-03-01 23:23:04 +01:00
d3bd2ff30d feat(skincare): allow HEIC/HEIF uploads in skin analysis 2026-03-01 23:23:04 +01:00
f1acfa21fc feat(routines): add inventory-aware product selection rules 2026-03-01 22:15:47 +01:00
914c6087bd fix(products): work around Gemini int-enum schema rejection in parse-text
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>
2026-03-01 22:00:48 +01:00
49c304d06f fix(routines): use system prompt for suggest and dedupe rules 2026-03-01 21:45:31 +01:00
cc657998e8 fix(llm): switch from thinking_budget to thinking_level=LOW for Gemini 3
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>
2026-03-01 20:15:49 +01:00
ada5f2a93b fix(llm): disable Gemini thinking to prevent MAX_TOKENS on structured output
Gemini 2.5 Flash (gemini-flash-latest) enables thinking by default.
Thinking tokens count toward max_output_tokens, leaving ~150 tokens for
actual JSON output and causing MAX_TOKENS truncation. Disable thinking
centrally in call_gemini via ThinkingConfig(thinking_budget=0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 20:12:31 +01:00
092fd87606 fix(llm): log and handle non-STOP finish_reason from Gemini
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>
2026-03-01 20:08:22 +01:00
75ef1bca56 feat(routines): add minoxidil beard/mustache option to routine suggestions
- 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>
2026-03-01 19:46:07 +01:00
78c67b6179 fix(backend): include steps in list_routines response
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>
2026-03-01 17:39:33 +01:00
5cb44b2c65 fix(backend): apply black/isort formatting and fix ruff noqa annotations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 17:27:07 +01:00
f72d5ba1b7 fix(models): add cascade delete-orphan to parent-child relationships
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>
2026-03-01 00:59:10 +01:00
81b1cacc5c refactor(llm): use response_schema with typed enums in all Gemini calls
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>
2026-03-01 00:46:23 +01:00
6e7f715ef2 feat: AI-generated skincare routine suggestions (single + batch)
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>
2026-03-01 00:34:43 +01:00
1b1566e6d7 feat(frontend): group products by category with ownership filter
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>
2026-02-28 23:07:37 +01:00
2691708304 fix(models): cascade delete inventory rows when product is deleted
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>
2026-02-28 22:48:00 +01:00
794650afc6 feat(models): add anti_aging to IngredientFunction enum
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>
2026-02-28 22:32:57 +01:00
a3753d0929 fix(backend): restore response_mime_type=json, raise max_output_tokens to 16384
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>
2026-02-28 22:26:41 +01:00
3fbf6d7041 fix(backend): drop response_mime_type=application/json to avoid constrained decoding
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>
2026-02-28 22:03:49 +01:00
26069f5d66 fix(backend): increase max_output_tokens to 65536, log finish_reason on error
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>
2026-02-28 21:57:12 +01:00
4abdc88286 fix(backend): request minified JSON from Gemini to avoid token truncation
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>
2026-02-28 21:49:26 +01:00
3e85858d41 fix(backend): sanitize NaN/Infinity/undefined in Gemini JSON response
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>
2026-02-28 21:46:47 +01:00
54903a3bed fix(backend): handle invalid/empty JSON from Gemini in product parse endpoint
- 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>
2026-02-28 21:43:39 +01:00
ac829171d9 feat(mcp): add FastMCP server with 14 tools for LLM agent access
- 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>
2026-02-28 17:59:11 +01:00
4954d4f449 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>
2026-02-28 13:25:57 +01:00
abf9593857 fix: correct Part.from_text() call and increase max_output_tokens for skin analysis
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>
2026-02-28 13:17:22 +01:00
66ee473deb feat: AI photo analysis for skin snapshots
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>
2026-02-28 12:47:51 +01:00
31e030eaac feat: AI pre-fill for product form via Gemini API
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>
2026-02-27 23:04:24 +01:00
43fcba4de6 feat: add full/empty weight fields to Product and last_weighed_at to ProductInventory
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>
2026-02-27 16:35:08 +01:00