fix(lang): pin the detected customer language explicitly (Spanish-heavy context was overriding plain mirror); None->mirror fallback
1316064 verified | from __future__ import annotations | |
| from app.prompts import build_system_prompt | |
| def test_system_prompt_includes_rules_and_brand(): | |
| p = build_system_prompt("TOORX Assist") | |
| assert "TOORX Assist" in p | |
| # language is the prominent RULE #1 | |
| assert "RULE #1" in p | |
| assert "language" in p.lower() | |
| # A DETECTED language is pinned explicitly (the Spanish-heavy context otherwise | |
| # drags the model into Spanish even for an English customer). The Hungarian bug | |
| # is fixed at the DETECTOR, not by refusing to pin. | |
| pinned = build_system_prompt("X", language="portugués de Portugal (pt-PT)") | |
| assert "pt-PT" in pinned | |
| # when detection is unsure (None) we fall back to mirroring | |
| assert "mirror" in build_system_prompt("X", language=None).lower() | |
| # anti-hallucination grounding | |
| assert "invent" in p.lower() | |
| # order verification (email + order number / postal code) | |
| assert "email" in p.lower() and "order number" in p.lower() and "postal code" in p.lower() | |
| assert "NEVER" in p # no full address / payment | |
| # tools referenced by name | |
| assert "escalate_to_human" in p | |
| assert "search_knowledge" in p and "search_products" in p and "lookup_order" in p | |
| # no emojis + don't translate identifiers | |
| assert "emoji" in p.lower() | |
| assert "variant_ids" in p | |
| def test_system_prompt_covers_attached_photo_product_search(): | |
| p = build_system_prompt("X") | |
| # the attached-file marker is explained and tied to the product search tool | |
| assert "[El cliente ha adjuntado" in p | |
| assert "CALL search_products" in p | |
| # attribute extraction is spelled out so the model searches with key terms | |
| assert "color" in p and "brand or model" in p | |
| # honest fallback when nothing similar exists | |
| assert "say so honestly" in p | |
| def test_system_prompt_default_brand(): | |
| assert "Asistente" in build_system_prompt("") | |
| def test_prompt_channel_handoff_is_mutually_exclusive(): | |
| web = build_system_prompt("X", channel="web") | |
| wa = build_system_prompt("X", channel="whatsapp") | |
| assert "show_form" in web and "WhatsApp" not in web | |
| assert "WhatsApp" in wa and "show_form" not in wa | |
| def test_brand_name_braces_are_sanitized(): | |
| # a brand name with { } must not break .format or inject placeholders | |
| p = build_system_prompt("Acme {evil}") | |
| assert "{evil}" not in p | |
| assert "Acme" in p | |
| def test_business_rules_are_top_priority_and_override_ficha(): | |
| """The merchant's custom_instructions must land HIGH in the prompt (before the | |
| GROUNDING block) and be framed as overriding the product sheet — otherwise weak | |
| models defer to the retrieved ficha (the 28mm-rule bug).""" | |
| rule = "Los discos de 28mm SI son compatibles con soporte de 25mm." | |
| p = build_system_prompt("TOORX", extra_instructions=rule) | |
| assert rule in p | |
| assert "BUSINESS RULES (HIGHEST PRIORITY" in p | |
| # the business block comes BEFORE the grounding block (position = weight) | |
| assert p.index("BUSINESS RULES") < p.index("# GROUNDING") | |
| # the grounding "say only what tools return" carries the override exception | |
| assert "a business rule" in p.lower() and "outranks the ficha" in p.lower() | |
| def test_no_business_block_when_no_custom_instructions(): | |
| p = build_system_prompt("TOORX") | |
| # the SECTION (with merchant content) is absent; the end-reminder mention of | |
| # "business rules" is harmless boilerplate, so check the section header. | |
| assert "these OVERRIDE the grounding" not in p | |
| def test_business_rules_truncation_raised_to_6000(): | |
| big = "x" * 5000 | |
| p = build_system_prompt("TOORX", extra_instructions=big) | |
| assert big in p # 5000-char rule set not silently cut (old cap was 2000) | |
| def test_shipping_cost_vs_time_routing_is_disambiguated(): | |
| """Shipping COST/zone -> get_shipping_cost; delivery TIME -> search_knowledge | |
| even when a destination is named (the 'cuánto tarda a Canarias' overlap).""" | |
| p = build_system_prompt("X") | |
| assert "get_shipping_cost" in p | |
| # the cost tool is explicitly scoped to zones/prices, not transit times | |
| assert "NOT transit times" in p | |
| # delivery-time questions with a destination are explicitly routed away | |
| assert "cuánto tarda a Canarias" in p | |