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.
This commit is contained in:
Piotr Oleszczyk 2026-03-04 11:52:07 +01:00
parent cfd2485b7e
commit 558708653c
3 changed files with 329 additions and 47 deletions

View file

@ -250,6 +250,9 @@ def test_suggest_routine(client, session):
kwargs = mock_gemini.call_args.kwargs
assert "function_handlers" in kwargs
assert "get_product_inci" in kwargs["function_handlers"]
assert "get_product_safety_rules" in kwargs["function_handlers"]
assert "get_product_actives" in kwargs["function_handlers"]
assert "get_product_usage_notes" in kwargs["function_handlers"]
def test_suggest_batch(client, session):

View file

@ -4,15 +4,20 @@ from datetime import date, timedelta
from sqlmodel import Session
from innercontext.api.routines import (
_build_actives_tool_handler,
_build_day_context,
_build_grooming_context,
_build_inci_tool_handler,
_build_objectives_context,
_build_products_context,
_build_recent_history,
_build_safety_rules_tool_handler,
_build_skin_context,
_build_usage_notes_tool_handler,
_contains_minoxidil_text,
_ev,
_extract_active_names,
_extract_requested_product_ids,
_get_available_products,
_is_minoxidil_product,
)
@ -336,3 +341,68 @@ def test_build_inci_tool_handler_returns_only_available_ids(session: Session):
assert products[0]["id"] == str(available.id)
assert products[0]["name"] == "Available"
assert products[0]["inci"] == ["Water", "Niacinamide"]
def test_extract_requested_product_ids_dedupes_and_limits():
ids = _extract_requested_product_ids(
{
"product_ids": [
"id-1",
"id-2",
"id-1",
3,
"id-3",
"id-4",
]
},
max_ids=3,
)
assert ids == ["id-1", "id-2", "id-3"]
def test_extract_active_names_uses_compact_distinct_names(session: Session):
p = Product(
id=uuid.uuid4(),
name="Test",
category="serum",
brand="Test",
recommended_time="both",
leave_on=True,
actives=[
{"name": "Niacinamide", "percent": 10},
{"name": "Niacinamide", "percent": 5},
{"name": "Zinc PCA", "percent": 1},
],
product_effect_profile={},
)
names = _extract_active_names(p)
assert names == ["Niacinamide", "Zinc PCA"]
def test_additional_tool_handlers_return_product_payloads(session: Session):
p = Product(
id=uuid.uuid4(),
name="Detail Product",
category="serum",
brand="Test",
recommended_time="both",
leave_on=True,
usage_notes="Apply morning and evening.",
actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}],
incompatible_with=[{"target": "Retinol", "scope": "same_step"}],
context_rules={"safe_after_shaving": True},
product_effect_profile={},
)
ids_payload = {"product_ids": [str(p.id)]}
actives_out = _build_actives_tool_handler([p])(ids_payload)
assert actives_out["products"][0]["actives"][0]["name"] == "Niacinamide"
notes_out = _build_usage_notes_tool_handler([p])(ids_payload)
assert notes_out["products"][0]["usage_notes"] == "Apply morning and evening."
safety_out = _build_safety_rules_tool_handler([p])(ids_payload)
assert "incompatible_with" in safety_out["products"][0]
assert "context_rules" in safety_out["products"][0]