| from __future__ import annotations |
|
|
| import pytest |
|
|
| from app.llm.base import ChatResult, ProviderError, ToolCall |
| from app.models import ChatMessage, ChatSession |
| from app.orchestrator import run_turn |
| from app.tools.registry import ToolContext |
|
|
|
|
| class FakeRouter: |
| def __init__(self, results: list[ChatResult]): |
| self._results = list(results) |
| self.calls = 0 |
|
|
| async def chat(self, messages, tools, tier="large", temperature=None): |
| self.calls += 1 |
| return self._results.pop(0) |
|
|
|
|
| class FakeShopify: |
| async def execute(self, query, variables=None): |
| return {"orders": {"edges": []}} |
|
|
|
|
| @pytest.fixture(autouse=True) |
| def no_real_search(monkeypatch): |
| async def fake_search(session, query, k=4, *, tenant_id=None): |
| return [] |
|
|
| from app.rag import index |
|
|
| monkeypatch.setattr(index, "search", fake_search) |
|
|
|
|
| async def _session(db): |
| s = ChatSession(shop="x") |
| db.add(s) |
| await db.flush() |
| return s |
|
|
|
|
| async def test_info_turn_uses_knowledge_tool_then_answers(db_session): |
| router = FakeRouter( |
| [ |
| ChatResult( |
| content=None, |
| tool_calls=[ToolCall(id="t1", name="search_knowledge", arguments={"query": "envíos"})], |
| finish_reason="tool_calls", |
| ), |
| ChatResult(content="Hacemos envíos gratis desde 199€.", tool_calls=[], finish_reason="stop"), |
| ] |
| ) |
| ctx = ToolContext(db=db_session, session=await _session(db_session)) |
| resp = await run_turn(router, ctx, "¿hacéis envíos gratis?", brand_name="Tienda") |
|
|
| assert resp.reply == "Hacemos envíos gratis desde 199€." |
| assert resp.used_tools == ["search_knowledge"] |
| assert router.calls == 2 |
|
|
| |
| from sqlalchemy import select |
|
|
| msgs = (await db_session.execute(select(ChatMessage))).scalars().all() |
| roles = [m.role for m in msgs] |
| assert "user" in roles and "assistant" in roles |
|
|
|
|
| async def test_order_turn_unverified_asks_for_credentials(db_session): |
| router = FakeRouter( |
| [ |
| ChatResult( |
| content=None, |
| tool_calls=[ToolCall(id="o1", name="lookup_order", arguments={})], |
| finish_reason="tool_calls", |
| ), |
| ChatResult( |
| content="Para localizar tu pedido necesito tu email y número de pedido.", |
| tool_calls=[], |
| finish_reason="stop", |
| ), |
| ] |
| ) |
| ctx = ToolContext(db=db_session, session=await _session(db_session), shopify=FakeShopify()) |
| resp = await run_turn(router, ctx, "¿dónde está mi pedido?") |
| assert "email" in resp.reply.lower() |
| assert resp.used_tools == ["lookup_order"] |
|
|
|
|
| class _ToolFailRouter: |
| """Fails (ProviderError) whenever tools are sent; answers when no tools.""" |
|
|
| def __init__(self, reply="Respuesta sin herramientas."): |
| self.reply = reply |
| self.calls_with_tools = 0 |
|
|
| async def chat(self, messages, tools, tier="large", temperature=None): |
| if tools: |
| self.calls_with_tools += 1 |
| raise ProviderError("tool_use_failed") |
| return ChatResult(content=self.reply, tool_calls=[], finish_reason="stop") |
|
|
|
|
| class _AlwaysFailRouter: |
| async def chat(self, messages, tools, tier="large", temperature=None): |
| raise ProviderError("down") |
|
|
|
|
| async def test_degrades_to_no_tools_when_tool_calling_fails(db_session): |
| router = _ToolFailRouter() |
| ctx = ToolContext(db=db_session, session=await _session(db_session)) |
| resp = await run_turn(router, ctx, "¿qué vendéis?") |
| assert resp.reply == "Respuesta sin herramientas." |
| assert router.calls_with_tools >= 1 |
|
|
|
|
| async def test_total_provider_failure_returns_fallback_not_500(db_session): |
| from app.orchestrator import FALLBACK_REPLY |
|
|
| ctx = ToolContext(db=db_session, session=await _session(db_session)) |
| resp = await run_turn(_AlwaysFailRouter(), ctx, "hola") |
| assert resp.reply == FALLBACK_REPLY |
|
|
|
|
| async def test_tool_budget_exhausted_forces_final_answer(db_session): |
| |
| loop_result = ChatResult( |
| content=None, |
| tool_calls=[ToolCall(id="t", name="search_knowledge", arguments={"query": "x"})], |
| finish_reason="tool_calls", |
| ) |
| router = FakeRouter( |
| [loop_result, loop_result, loop_result, loop_result, |
| ChatResult(content="Respuesta final.", tool_calls=[], finish_reason="stop")] |
| ) |
| ctx = ToolContext(db=db_session, session=await _session(db_session)) |
| resp = await run_turn(router, ctx, "bucle") |
| assert resp.reply == "Respuesta final." |
| assert router.calls == 5 |
|
|