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 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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue