feat(backend): include last-used date in product LLM details
This commit is contained in:
parent
40d26514a1
commit
7a66a7911d
5 changed files with 91 additions and 7 deletions
|
|
@ -1,8 +1,11 @@
|
||||||
|
from datetime import date
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from google.genai import types as genai_types
|
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:
|
def _ev(v: object) -> str:
|
||||||
|
|
@ -72,7 +75,12 @@ def _build_compact_actives_payload(product: Product) -> list[dict[str, object]]:
|
||||||
return payload[:24]
|
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()
|
ctx = product.to_llm_context()
|
||||||
inci = product.inci or []
|
inci = product.inci or []
|
||||||
|
|
||||||
|
|
@ -91,11 +99,43 @@ def _map_product_details(product: Product, pid: str) -> dict[str, object]:
|
||||||
"safety": ctx.get("safety") or {},
|
"safety": ctx.get("safety") or {},
|
||||||
"min_interval_hours": ctx.get("min_interval_hours"),
|
"min_interval_hours": ctx.get("min_interval_hours"),
|
||||||
"max_frequency_per_week": ctx.get("max_frequency_per_week"),
|
"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}
|
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]:
|
def _handler(args: dict[str, Any]) -> dict[str, object]:
|
||||||
requested_ids = _extract_requested_product_ids(args)
|
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)
|
product = available_by_id.get(pid)
|
||||||
if product is None:
|
if product is None:
|
||||||
continue
|
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 {"products": products_payload}
|
||||||
|
|
||||||
return _handler
|
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, "
|
"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, "
|
"or usage cadence. Returns per-product fields: id, name, brand, category, recommended_time, "
|
||||||
"leave_on, targets, effect_profile, inci, actives, context_rules, safety, "
|
"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(
|
parameters=genai_types.Schema(
|
||||||
type=genai_types.Type.OBJECT,
|
type=genai_types.Type.OBJECT,
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ from innercontext.api.llm_context import build_user_profile_context
|
||||||
from innercontext.api.product_llm_tools import (
|
from innercontext.api.product_llm_tools import (
|
||||||
PRODUCT_DETAILS_FUNCTION_DECLARATION,
|
PRODUCT_DETAILS_FUNCTION_DECLARATION,
|
||||||
_extract_requested_product_ids as _shared_extract_requested_product_ids,
|
_extract_requested_product_ids as _shared_extract_requested_product_ids,
|
||||||
|
build_last_used_on_by_product,
|
||||||
build_product_details_tool_handler,
|
build_product_details_tool_handler,
|
||||||
)
|
)
|
||||||
from innercontext.llm import (
|
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)):
|
def suggest_shopping(session: Session = Depends(get_session)):
|
||||||
context = _build_shopping_context(session, reference_date=date.today())
|
context = _build_shopping_context(session, reference_date=date.today())
|
||||||
shopping_products = _get_shopping_products(session)
|
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 = (
|
prompt = (
|
||||||
f"Na podstawie poniższych danych przeanalizuj, jakie TYPY produktów "
|
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 = {
|
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:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from innercontext.api.llm_context import build_user_profile_context
|
||||||
from innercontext.api.product_llm_tools import (
|
from innercontext.api.product_llm_tools import (
|
||||||
PRODUCT_DETAILS_FUNCTION_DECLARATION,
|
PRODUCT_DETAILS_FUNCTION_DECLARATION,
|
||||||
_extract_requested_product_ids as _shared_extract_requested_product_ids,
|
_extract_requested_product_ids as _shared_extract_requested_product_ids,
|
||||||
|
build_last_used_on_by_product,
|
||||||
build_product_details_tool_handler,
|
build_product_details_tool_handler,
|
||||||
)
|
)
|
||||||
from innercontext.llm import (
|
from innercontext.llm import (
|
||||||
|
|
@ -602,6 +603,10 @@ def suggest_routine(
|
||||||
session,
|
session,
|
||||||
time_filter=data.part_of_day.value,
|
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)
|
objectives_ctx = _build_objectives_context(data.include_minoxidil_beard)
|
||||||
|
|
||||||
mode_line = "MODE: standard"
|
mode_line = "MODE: standard"
|
||||||
|
|
@ -646,7 +651,10 @@ def suggest_routine(
|
||||||
)
|
)
|
||||||
|
|
||||||
function_handlers = {
|
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:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -154,3 +154,24 @@ def test_shopping_tool_handlers_return_payloads(session: Session):
|
||||||
assert details["products"][0]["inci"] == ["Water", "Niacinamide"]
|
assert details["products"][0]["inci"] == ["Water", "Niacinamide"]
|
||||||
assert details["products"][0]["actives"][0]["name"] == "Niacinamide"
|
assert details["products"][0]["actives"][0]["name"] == "Niacinamide"
|
||||||
assert "context_rules" in details["products"][0]
|
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"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
details_out = build_product_details_tool_handler([p])(ids_payload)
|
||||||
assert details_out["products"][0]["actives"][0]["name"] == "Niacinamide"
|
assert details_out["products"][0]["actives"][0]["name"] == "Niacinamide"
|
||||||
assert "context_rules" in details_out["products"][0]
|
assert "context_rules" in details_out["products"][0]
|
||||||
|
assert details_out["products"][0]["last_used_on"] is None
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue