Commit graph

58 commits

Author SHA1 Message Date
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
9bbc34ffd2 test(api): fix ruff issues in routine tests 2026-03-04 02:23:19 +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
5ad9b66a21 build(backend): add pytest-cov configuration and report generation 2026-03-03 22:06:24 +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
3c1dcbeb06 feat(backend): add Alembic migrations
- 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>
2026-02-28 20:14:57 +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
e60dee5015 style: reformat import block in main.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 23:05:14 +01:00