feat(profile): add profile settings and LLM user context

This commit is contained in:
Piotr Oleszczyk 2026-03-05 15:57:21 +01:00
parent db3d9514d5
commit b99b9ed68e
25 changed files with 472 additions and 9 deletions

View file

@ -0,0 +1,23 @@
from datetime import date
from sqlmodel import Session
from innercontext.api.llm_context import build_user_profile_context
from innercontext.models import SexAtBirth, UserProfile
def test_build_user_profile_context_without_data(session: Session):
ctx = build_user_profile_context(session, reference_date=date(2026, 3, 5))
assert ctx == "USER PROFILE: no data\n"
def test_build_user_profile_context_with_data(session: Session):
profile = UserProfile(birth_date=date(1990, 3, 20), sex_at_birth=SexAtBirth.FEMALE)
session.add(profile)
session.commit()
ctx = build_user_profile_context(session, reference_date=date(2026, 3, 5))
assert "USER PROFILE:" in ctx
assert "Age: 35" in ctx
assert "Birth date: 1990-03-20" in ctx
assert "Sex at birth: female" in ctx

View file

@ -11,15 +11,26 @@ from innercontext.api.products import (
_build_shopping_context,
_extract_requested_product_ids,
)
from innercontext.models import Product, ProductInventory, SkinConditionSnapshot
from innercontext.models import (
Product,
ProductInventory,
SexAtBirth,
SkinConditionSnapshot,
)
from innercontext.models.profile import UserProfile
def test_build_shopping_context(session: Session):
# Empty context
ctx = _build_shopping_context(session)
ctx = _build_shopping_context(session, reference_date=date.today())
assert "USER PROFILE: no data" in ctx
assert "(brak danych)" in ctx
assert "POSIADANE PRODUKTY" in ctx
profile = UserProfile(birth_date=date(1990, 1, 10), sex_at_birth=SexAtBirth.MALE)
session.add(profile)
session.commit()
# Add snapshot
snap = SkinConditionSnapshot(
id=uuid.uuid4(),
@ -54,7 +65,10 @@ def test_build_shopping_context(session: Session):
session.add(inv)
session.commit()
ctx = _build_shopping_context(session)
ctx = _build_shopping_context(session, reference_date=date(2026, 3, 5))
assert "USER PROFILE:" in ctx
assert "Age: 36" in ctx
assert "Sex at birth: male" in ctx
assert "Typ skóry: combination" in ctx
assert "Nawilżenie: 3/5" in ctx
assert "Wrażliwość: 4/5" in ctx
@ -91,6 +105,7 @@ def test_suggest_shopping(client, session):
assert data["suggestions"][0]["product_type"] == "cleanser"
assert data["reasoning"] == "Test shopping"
kwargs = mock_gemini.call_args.kwargs
assert "USER PROFILE:" in kwargs["contents"]
assert "function_handlers" in kwargs
assert "get_product_inci" in kwargs["function_handlers"]
assert "get_product_safety_rules" in kwargs["function_handlers"]
@ -111,7 +126,7 @@ def test_shopping_context_medication_skip(session: Session):
session.add(p)
session.commit()
ctx = _build_shopping_context(session)
ctx = _build_shopping_context(session, reference_date=date.today())
assert "Epiduo" not in ctx

View file

@ -0,0 +1,35 @@
def test_get_profile_empty(client):
r = client.get("/profile")
assert r.status_code == 200
assert r.json() is None
def test_upsert_profile_create_and_get(client):
create = client.patch(
"/profile", json={"birth_date": "1990-01-15", "sex_at_birth": "male"}
)
assert create.status_code == 200
body = create.json()
assert body["birth_date"] == "1990-01-15"
assert body["sex_at_birth"] == "male"
fetch = client.get("/profile")
assert fetch.status_code == 200
fetched = fetch.json()
assert fetched is not None
assert fetched["id"] == body["id"]
def test_upsert_profile_updates_existing_row(client):
first = client.patch(
"/profile", json={"birth_date": "1992-06-10", "sex_at_birth": "female"}
)
assert first.status_code == 200
first_id = first.json()["id"]
second = client.patch("/profile", json={"sex_at_birth": "intersex"})
assert second.status_code == 200
second_body = second.json()
assert second_body["id"] == first_id
assert second_body["birth_date"] == "1992-06-10"
assert second_body["sex_at_birth"] == "intersex"

View file

@ -248,6 +248,7 @@ def test_suggest_routine(client, session):
assert data["steps"][0]["action_type"] == "shaving_razor"
assert data["reasoning"] == "because"
kwargs = mock_gemini.call_args.kwargs
assert "USER PROFILE:" in kwargs["contents"]
assert "function_handlers" in kwargs
assert "get_product_inci" in kwargs["function_handlers"]
assert "get_product_safety_rules" in kwargs["function_handlers"]
@ -279,6 +280,8 @@ def test_suggest_batch(client, session):
assert len(data["days"]) == 1
assert data["days"][0]["date"] == "2026-03-03"
assert data["overall_reasoning"] == "batch test"
kwargs = mock_gemini.call_args.kwargs
assert "USER PROFILE:" in kwargs["contents"]
def test_suggest_batch_invalid_date_range(client):

View file

@ -128,3 +128,33 @@ def test_delete_snapshot(client):
def test_delete_snapshot_not_found(client):
r = client.delete(f"/skincare/{uuid.uuid4()}")
assert r.status_code == 404
def test_analyze_photos_includes_user_profile_context(client, monkeypatch):
from innercontext.api import skincare as skincare_api
captured: dict[str, object] = {}
class _FakeResponse:
text = "{}"
def _fake_call_gemini(**kwargs):
captured.update(kwargs)
return _FakeResponse()
monkeypatch.setattr(skincare_api, "call_gemini", _fake_call_gemini)
profile = client.patch(
"/profile", json={"birth_date": "1991-02-10", "sex_at_birth": "female"}
)
assert profile.status_code == 200
r = client.post(
"/skincare/analyze-photos",
files={"photos": ("face.jpg", b"fake-bytes", "image/jpeg")},
)
assert r.status_code == 200
parts = captured["contents"]
assert isinstance(parts, list)
assert any("USER PROFILE:" in str(part) for part in parts)