Brave como respaldo de DuckDuckGo, y arreglo de la guardia de bf16

Cadena de búsqueda con respaldo. DuckDuckGo sigue de primario porque no exige
credenciales: en el camino habitual ningún tercero se entera de qué busca la
familia. Pero su endpoint lite no es una API con contrato, así que cuando
limite por tasa o cambie el HTML, Brave responde.

Dos matices del encadenado:

- Cero resultados NO dispara el respaldo. Si el primario respondió bien y no
  encontró nada, esa es la respuesta correcta; encadenar gastaría cuota y
  devolvería resultados peores. Solo se avanza ante un error real.
- La cadena recuerda quién respondió: al depurar una respuesta rara, lo
  primero que hay que saber es de dónde salió.

Un respaldo sin credencial falla al arrancar y no en la primera consulta —
que es justo cuando el primario ya falló y el respaldo tiene que funcionar.

Aparte, verificando torch en la 2060 apareció un defecto real en backends.py:
torch.cuda.is_bf16_supported() usa including_emulation=True por defecto, así
que devuelve True en Turing, donde bf16 no existe en silicio. La guardia no
habría atrapado el perfil de la 5090 corriendo en la 2060: el entrenamiento
seguía, emulado y silencioso. Medido en la placa: 4,49 ms por matmul en bf16
contra 1,75 ms en fp16, unas 2,6x más lento. Ahora se consulta con
including_emulation=False, con respaldo por compute capability.

11 tests nuevos (113 en total).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 00:39:13 -03:00
parent 5b9f2a58bc
commit 36040d7cc8
8 changed files with 433 additions and 43 deletions
+120 -2
View File
@@ -12,6 +12,8 @@ from pathlib import Path
import pytest
from enlace.agent.tools.search import (
BraveBackend,
CadenaDeBackends,
DuckDuckGoBackend,
SearchError,
SearchResult,
@@ -176,12 +178,18 @@ def test_consulta_vacia_da_error_util():
# --- config ---------------------------------------------------------------
def test_la_config_por_defecto_no_necesita_credenciales():
def test_el_primario_no_necesita_credenciales():
"""Lo que importa del default: la primera consulta sale sin credenciales.
La cadena puede tener respaldos que sí las exijan, pero el camino habitual
no le informa a ningún tercero qué busca la familia.
"""
cfg = load_agent_config()
assert cfg.search.backend == "duckduckgo"
assert cfg.search.region == "es-es"
backend = build_backend(cfg.search)
assert isinstance(backend, DuckDuckGoBackend)
primero = backend.backends[0] if isinstance(backend, CadenaDeBackends) else backend
assert isinstance(primero, DuckDuckGoBackend)
def test_searxng_exige_url():
@@ -204,3 +212,113 @@ def test_el_presupuesto_de_contexto_tiene_un_tope_razonable():
def test_config_de_agente_inexistente_da_error_util(tmp_path):
with pytest.raises(ConfigError, match="no existe"):
load_agent_config(tmp_path / "no-existe.yaml")
# --- cadena de respaldo ----------------------------------------------------
class BackendQueFalla:
name = "roto"
def __init__(self, mensaje: str = "caído") -> None:
self.mensaje = mensaje
self.llamadas = 0
def search(self, query: str, max_results: int) -> list[SearchResult]:
self.llamadas += 1
raise SearchError(self.mensaje)
def test_la_cadena_usa_el_primario_cuando_anda():
primario = BackendFalso([SearchResult("T", "https://e.com", "c")])
respaldo = BackendFalso([SearchResult("R", "https://r.com", "c")])
cadena = CadenaDeBackends([primario, respaldo])
assert cadena.search("q", 5)[0].title == "T"
assert respaldo.llamadas == 0 # no se toca la cuota del respaldo
assert cadena.ultimo_backend == "falso"
def test_la_cadena_cae_al_respaldo_ante_un_error():
roto = BackendQueFalla("límite de tasa alcanzado")
respaldo = BackendFalso([SearchResult("R", "https://r.com", "c")])
cadena = CadenaDeBackends([roto, respaldo])
assert cadena.search("q", 5)[0].title == "R"
assert roto.llamadas == 1
def test_cero_resultados_NO_dispara_el_respaldo():
"""Si el primario respondió bien y no encontró nada, esa es la respuesta.
Encadenar al siguiente proveedor por esto gastaría cuota y devolvería
resultados peores para una consulta que genuinamente no tiene respuesta.
"""
vacio = BackendFalso([])
respaldo = BackendFalso([SearchResult("R", "https://r.com", "c")])
cadena = CadenaDeBackends([vacio, respaldo])
assert cadena.search("consulta sin resultados", 5) == []
assert respaldo.llamadas == 0
def test_si_fallan_todos_el_error_dice_por_que_cada_uno():
cadena = CadenaDeBackends([BackendQueFalla("429"), BackendQueFalla("timeout")])
with pytest.raises(SearchError) as exc:
cadena.search("q", 5)
assert "429" in str(exc.value) and "timeout" in str(exc.value)
def test_la_cadena_recuerda_quien_respondio():
"""Al depurar una respuesta rara, lo primero es saber de dónde salió."""
respaldo = BackendFalso([SearchResult("R", "https://r.com", "c")])
respaldo.name = "respaldo"
cadena = CadenaDeBackends([BackendQueFalla(), respaldo])
cadena.search("q", 5)
assert cadena.ultimo_backend == "respaldo"
def test_una_cadena_vacia_es_un_error():
with pytest.raises(SearchError, match="ningún backend"):
CadenaDeBackends([])
# --- construcción desde config ---------------------------------------------
def test_un_solo_backend_no_se_envuelve_en_cadena():
cfg = SearchConfig(backend="duckduckgo")
assert isinstance(build_backend(cfg), DuckDuckGoBackend)
def test_config_con_fallback_arma_la_cadena():
cfg = SearchConfig(backend="duckduckgo", fallbacks=["brave"], brave_api_key="x")
backend = build_backend(cfg)
assert isinstance(backend, CadenaDeBackends)
assert [type(b).__name__ for b in backend.backends] == [
"DuckDuckGoBackend",
"BraveBackend",
]
def test_el_fallback_repetido_no_se_duplica():
cfg = SearchConfig(backend="duckduckgo", fallbacks=["duckduckgo", "brave"], brave_api_key="x")
assert cfg.cadena == ["duckduckgo", "brave"]
def test_un_fallback_sin_credencial_falla_al_arrancar():
"""No en la primera consulta real — que es justo cuando el primario ya
falló y el respaldo tiene que funcionar."""
with pytest.raises(ValueError, match="fallback 'brave'"):
SearchConfig(backend="duckduckgo", fallbacks=["brave"])
def test_brave_como_primario_tambien_exige_credencial():
with pytest.raises(ValueError, match="backend 'brave'"):
SearchConfig(backend="brave")
def test_la_config_del_repo_declara_brave_de_respaldo():
cfg = load_agent_config()
assert cfg.search.backend == "duckduckgo"
assert "brave" in cfg.search.fallbacks