Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions docs/plan-assistant-genai.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,40 @@ se informa del número de llamadas y del modelo**, y se espera.

---

## PR 4b — Modelo de embeddings (se decide con datos)

PR aparte, después del PR 4. **Qué:** decidir si se cambia el modelo de embeddings actual,
`paraphrase-multilingual-MiniLM-L12-v2`. Ya es multilingüe, así que el motivo no es el idioma.
**Por qué:** con ese modelo, el SKU aceptable de q06 y q10 queda fuera del top 10 de la recuperación.
Eso pone un techo de 8/10 a E1–E3, por mucho que acierte el selector. La causa es que los nombres
comerciales confunden («Bota **Glaciar**», «Tienda **Refugio**») y que la respuesta exige una
inferencia: pasar de «no resbalar en hielo» a crampones, y de «cargar a la espalda» a mochila.

Comprobación gratuita en memoria (sin llamadas a Claude y con el dataset sin cambios). Posiciones
del SKU aceptable:

| Modelo | Dims | Preguntas con un SKU aceptable en el top 10 | q05 | q06 | q10 | Migración |
|---|---|---|---|---|---|---|
| MiniLM-L12 (actual) | 384 | 8/10 | 7 | 11 | 16 | — |
| multilingual-e5-small | 384 | 9/10 | 15 | 7 | 3 | no |
| multilingual-e5-base | 768 | 8/10 | 16 | 15 | 4 | sí |
| multilingual-e5-large | 1024 | 10/10 | 10 (justo) | 4 | 2 | sí |

- **e5-small y e5-base:** no están en la lista de fastembed 0.9.1. Se cargan con
`add_custom_model`, a partir del ONNX oficial de Hugging Face y con pooling *mean*.
- **Las tres e5:** usan los prefijos `query:` y `passage:`.
- **e5-large:** pesa 2,24 GB frente a 0,22 GB; la imagen crece en consecuencia.

**Cómo se decide:**
- **Línea base:** E1–E3 con el modelo actual.
- **Comparación:** la recuperación de cada candidato.
- **Criterio:** e5-small evita la migración, pero pierde q05. e5-large recupera las diez preguntas,
a cambio de la migración a `vector(1024)`, el tamaño de la imagen y la latencia.
- **Si se cambia de modelo:** migración, prefijos en el adaptador, reindexado, y E1–E3 otra vez
contra la línea base.

---

## PR 5 — Evaluador jev

Añade un segundo `CandidateAssessor` sobre un modelo de decisión. Nada fuera del adaptador cambia:
Expand Down
16 changes: 16 additions & 0 deletions docs/step-18-assistant-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,22 @@ cubre ambas preguntas con 300 llamadas y deja el total en unas **930**.
top 10 de la recuperación en q06 (`MT-0020`) ni en q10 (`MT-0017`). En q09 faltan 2 de sus 4.
Por eso E1–E3 llegan como mucho a 8/10, y el informe lo muestra en la columna `reachable`.

### Línea base (decisión del usuario)

- **Modelo de embeddings:** E1–E3 se ejecutan con el modelo actual,
`paraphrase-multilingual-MiniLM-L12-v2`. Es multilingüe; se comprobó en la configuración, en el
Dockerfile y en el catálogo de fastembed.
- **Dataset:** no cambia.
- **Informe:** da dos números.
- **Extremo a extremo:** aciertos sobre 10, junto al techo de recuperación (8/10: en q06 y q10
no hay ningún SKU aceptable entre los candidatos).
- **Solo selector:** aciertos contando únicamente las preguntas que tienen un SKU aceptable entre
los candidatos.
- **Cambio de modelo:** queda como PR 4b del plan, y se decidirá con estos datos.
- **Ejecución pendiente:** el primer intento se paró en la primera tanda de peticiones: la API
respondió 400 por falta de crédito. Ninguna de esas peticiones se facturó. Se repite cuando haya
crédito.

## Correcciones tras la code review (3b)

La revisión (`/code-review`, lanzada por el usuario) dio 10 hallazgos y se comprobó cada uno contra el
Expand Down
14 changes: 14 additions & 0 deletions tests/evals/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ def reachable(self) -> bool:
)


def baseline_lines(runs: Sequence[SelectorRun]) -> tuple[str, str]:
reachable = [run for run in runs if run.reachable]
unreachable = [run.question.id for run in runs if not run.reachable]
ceiling = f"retrieval ceiling {len(reachable)}/{len(runs)}"
if unreachable:
ceiling += f" ({', '.join(unreachable)}: no acceptable SKU among the candidates)"
end_to_end = accuracy_line([r.question for r in runs], [r.outcome for r in runs])
selector_only = accuracy_line([r.question for r in reachable], [r.outcome for r in reachable])
return (
f"end-to-end {end_to_end}; {ceiling}",
f"selector only {selector_only} (questions with an acceptable SKU among the candidates)",
)


def threshold_table(runs: Sequence[SelectorRun]) -> list[ThresholdRow]:
rows = []
for requirements_threshold, min_score in grid(THRESHOLD_GRID, THRESHOLD_GRID):
Expand Down
32 changes: 32 additions & 0 deletions tests/evals/test_eval_scoring.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
EvalQuestion,
SelectorRun,
accuracy_line,
baseline_lines,
is_correct,
load_dataset,
outcome_of,
Expand Down Expand Up @@ -131,6 +132,26 @@ def test_an_acceptable_sku_outside_the_candidates_is_unreachable(self) -> None:
assert run.reachable is False
assert replace(run, question=NO_RECOMMENDATION).reachable is True

def test_baseline_gives_end_to_end_and_selector_only(self) -> None:
good = _candidate("MT-0002", 0.9)
missing = _candidate("MT-0001", 0.9)
runs = [
_run(RECOMMEND, "chosen:MT-0002", good),
_run(replace(RECOMMEND, id="q06"), "chosen:MT-0001", missing),
_run(NO_RECOMMENDATION, "below_threshold", missing),
]

end_to_end, selector_only = baseline_lines(runs)

assert end_to_end == (
"end-to-end 2/3: recommend 1/2, no_recommendation 1/1; "
"retrieval ceiling 2/3 (q06: no acceptable SKU among the candidates)"
)
assert selector_only == (
"selector only 2/2: recommend 1/1, no_recommendation 1/1 "
"(questions with an acceptable SKU among the candidates)"
)

async def test_api_calls_flag_many_rate_limits(self) -> None:
calls = ApiCalls(requests=20)
calls.statuses[200] = 19
Expand All @@ -151,3 +172,14 @@ def _candidate(sku: str, score: float) -> Candidate:
content=sku,
retrieval_score=score,
)


def _run(question: EvalQuestion, outcome: str, candidate: Candidate) -> SelectorRun:
return SelectorRun(
question=question,
outcome=outcome,
latency_ms=1,
candidates=(candidate,),
assessments=(),
unassessed=0,
)
7 changes: 3 additions & 4 deletions tests/evals/test_eval_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
RecordingAssessor,
SelectorRun,
Tokens,
accuracy_line,
assessment_tokens,
baseline_lines,
candidate_of,
latency_line,
outcome_of,
Expand Down Expand Up @@ -105,14 +105,13 @@ async def _run_selector(
def _print(title: str, report: SelectorReport, baseline: SelectorReport | None = None) -> None:
runs = report.runs
print(f"\n{title}")
print(f" result {accuracy_line([r.question for r in runs], [r.outcome for r in runs])}")
for line in baseline_lines(runs):
print(f" {line}")
if baseline is not None:
same = sum(
run.outcome == base.outcome for run, base in zip(runs, baseline.runs, strict=True)
)
print(f" same outcome as E1: {same}/{len(runs)}")
reachable = sum(run.reachable for run in runs)
print(f" retrieval ceiling: an acceptable outcome is reachable in {reachable}/{len(runs)}")
for run in runs:
verdict = "ok" if run.correct else "WRONG"
print(
Expand Down
Loading