feat(backend): include last-used date in product LLM details

This commit is contained in:
Piotr Oleszczyk 2026-03-05 16:48:49 +01:00
parent 40d26514a1
commit 7a66a7911d
5 changed files with 91 additions and 7 deletions

View file

@ -1,8 +1,11 @@
from datetime import date
from typing import Any
from uuid import UUID
from google.genai import types as genai_types
from sqlmodel import Session, col, select
from innercontext.models import Product
from innercontext.models import Product, Routine, RoutineStep
def _ev(v: object) -> str:
@ -72,7 +75,12 @@ def _build_compact_actives_payload(product: Product) -> list[dict[str, object]]:
return payload[:24]
def _map_product_details(product: Product, pid: str) -> dict[str, object]:
def _map_product_details(
product: Product,
pid: str,
*,
last_used_on: date | None = None,
) -> dict[str, object]:
ctx = product.to_llm_context()
inci = product.inci or []
@ -91,11 +99,43 @@ def _map_product_details(product: Product, pid: str) -> dict[str, object]:
"safety": ctx.get("safety") or {},
"min_interval_hours": ctx.get("min_interval_hours"),
"max_frequency_per_week": ctx.get("max_frequency_per_week"),
"last_used_on": last_used_on.isoformat() if last_used_on else None,
}
def build_product_details_tool_handler(products: list[Product]):
def build_last_used_on_by_product(
session: Session,
product_ids: list[UUID],
) -> dict[str, date]:
if not product_ids:
return {}
rows = session.exec(
select(RoutineStep, Routine)
.join(Routine)
.where(col(RoutineStep.product_id).in_(product_ids))
.order_by(col(Routine.routine_date).desc())
).all()
last_used: dict[str, date] = {}
for step, routine in rows:
product_id = step.product_id
if product_id is None:
continue
key = str(product_id)
if key in last_used:
continue
last_used[key] = routine.routine_date
return last_used
def build_product_details_tool_handler(
products: list[Product],
*,
last_used_on_by_product: dict[str, date] | None = None,
):
available_by_id = {str(p.id): p for p in products}
last_used_on_by_product = last_used_on_by_product or {}
def _handler(args: dict[str, Any]) -> dict[str, object]:
requested_ids = _extract_requested_product_ids(args)
@ -104,7 +144,13 @@ def build_product_details_tool_handler(products: list[Product]):
product = available_by_id.get(pid)
if product is None:
continue
products_payload.append(_map_product_details(product, pid))
products_payload.append(
_map_product_details(
product,
pid,
last_used_on=last_used_on_by_product.get(pid),
)
)
return {"products": products_payload}
return _handler
@ -117,7 +163,7 @@ PRODUCT_DETAILS_FUNCTION_DECLARATION = genai_types.FunctionDeclaration(
"Call it when you need to verify ingredient conflicts, irritation risk, barrier compatibility, "
"or usage cadence. Returns per-product fields: id, name, brand, category, recommended_time, "
"leave_on, targets, effect_profile, inci, actives, context_rules, safety, "
"min_interval_hours, and max_frequency_per_week."
"min_interval_hours, max_frequency_per_week, and last_used_on (ISO date or null)."
),
parameters=genai_types.Schema(
type=genai_types.Type.OBJECT,

View file

@ -16,6 +16,7 @@ from innercontext.api.llm_context import build_user_profile_context
from innercontext.api.product_llm_tools import (
PRODUCT_DETAILS_FUNCTION_DECLARATION,
_extract_requested_product_ids as _shared_extract_requested_product_ids,
build_last_used_on_by_product,
build_product_details_tool_handler,
)
from innercontext.llm import (
@ -922,6 +923,10 @@ Format odpowiedzi - zwróć wyłącznie JSON zgodny z podanym schematem."""
def suggest_shopping(session: Session = Depends(get_session)):
context = _build_shopping_context(session, reference_date=date.today())
shopping_products = _get_shopping_products(session)
last_used_on_by_product = build_last_used_on_by_product(
session,
product_ids=[p.id for p in shopping_products],
)
prompt = (
f"Na podstawie poniższych danych przeanalizuj, jakie TYPY produktów "
@ -956,7 +961,10 @@ def suggest_shopping(session: Session = Depends(get_session)):
)
function_handlers = {
"get_product_details": build_product_details_tool_handler(shopping_products),
"get_product_details": build_product_details_tool_handler(
shopping_products,
last_used_on_by_product=last_used_on_by_product,
),
}
try:

View file

@ -14,6 +14,7 @@ from innercontext.api.llm_context import build_user_profile_context
from innercontext.api.product_llm_tools import (
PRODUCT_DETAILS_FUNCTION_DECLARATION,
_extract_requested_product_ids as _shared_extract_requested_product_ids,
build_last_used_on_by_product,
build_product_details_tool_handler,
)
from innercontext.llm import (
@ -602,6 +603,10 @@ def suggest_routine(
session,
time_filter=data.part_of_day.value,
)
last_used_on_by_product = build_last_used_on_by_product(
session,
product_ids=[p.id for p in available_products],
)
objectives_ctx = _build_objectives_context(data.include_minoxidil_beard)
mode_line = "MODE: standard"
@ -646,7 +651,10 @@ def suggest_routine(
)
function_handlers = {
"get_product_details": build_product_details_tool_handler(available_products),
"get_product_details": build_product_details_tool_handler(
available_products,
last_used_on_by_product=last_used_on_by_product,
),
}
try:

View file

@ -154,3 +154,24 @@ def test_shopping_tool_handlers_return_payloads(session: Session):
assert details["products"][0]["inci"] == ["Water", "Niacinamide"]
assert details["products"][0]["actives"][0]["name"] == "Niacinamide"
assert "context_rules" in details["products"][0]
assert details["products"][0]["last_used_on"] is None
def test_shopping_tool_handler_includes_last_used_on_from_mapping(session: Session):
product = Product(
id=uuid.uuid4(),
name="Mapped Product",
brand="Brand",
category="serum",
recommended_time="both",
leave_on=True,
product_effect_profile={},
)
payload = {"product_ids": [str(product.id)]}
details = build_product_details_tool_handler(
[product],
last_used_on_by_product={str(product.id): date(2026, 3, 1)},
)(payload)
assert details["products"][0]["last_used_on"] == "2026-03-01"

View file

@ -393,3 +393,4 @@ def test_product_details_tool_handler_returns_product_payloads(session: Session)
details_out = build_product_details_tool_handler([p])(ids_payload)
assert details_out["products"][0]["actives"][0]["name"] == "Niacinamide"
assert "context_rules" in details_out["products"][0]
assert details_out["products"][0]["last_used_on"] is None