Skip to main content
Glama
README.md
# Tutor de Polaco con Modelo Local y MCP

Este manual explica cómo configurar y utilizar el sistema de tutoría de polaco con un modelo de lenguaje grande (LLM) ejecutándose localmente en tu máquina, integrado con VS Code a través del protocolo MCP (Model Context Protocol).

## 1. ¿Qué es esto?


Este proyecto implementa un tutor de polaco que utiliza un servidor MCP para comunicarse con un modelo de IA. El objetivo es proporcionar una experiencia de aprendizaje interactiva y personalizada sin depender de servicios en la nube, ahorrando tokens y garantizando la privacidad.

-   **MCP (Model Context Protocol)**: Un estándar abierto para que los modelos de IA interactúen con herramientas externas. Aquí, el servidor MCP (`mcp/server.py`) expone las herramientas pedagógicas del tutor (buscar conceptos, corregir respuestas, etc.).
-   **Modelo Local (Ollama)**: Ejecutamos un LLM directamente en tu máquina (ej. `qwen3:8b`) para procesar las solicitudes y usar las herramientas MCP.
-   **Continue (Extensión VS Code)**: Actúa como el cliente que conecta VS Code con el modelo local y el servidor MCP, permitiendo la interacción a través del chat.

## 2. Qué hay dentro


| | |
|---|---|
| Corpus | 1.425 páginas estructuradas (100 %) |
| Grafo | 15.748 nodos. 75.156 aristas |
| Conceptos | 283 curados, 253 con material del corpus (89%) |
| Respuestas | 2.918 ítems con la clave **del propio libro**. no inferidas |
| Evaluación | 360 nodos reservados, nunca usados para practicar |

## 3. Requisitos Previos


Antes de empezar, asegúrate de tener lo siguiente instalado:

-   **VS Code**: El editor de código.
-   **Python 3.10+**: Para ejecutar el servidor MCP. Se recomienda usar el entorno virtual del proyecto (`.venv`).
-   **Homebrew**: Gestor de paquetes para macOS (si no lo tienes, instálalo desde [brew.sh](https://brew.sh/)).
-   **Git**: Para clonar el repositorio.

## 4. Instalación


Sigue estos pasos para configurar el entorno:

### 4.1. Clonar el Repositorio

```bash
git clone https://github.com/tu_usuario/polski-tutor.git
cd polski-tutor
```

### 4.2. Entorno Python

Configura el entorno virtual del proyecto:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt # Si existe requirements.txt, o usa pyproject.toml
```

### 4.3. Instalar Ollama

Ollama es el motor que ejecuta modelos de lenguaje localmente.

```bash
# Instalar Ollama vía Homebrew
brew install ollama

# Iniciar el servicio de Ollama (se ejecutará en segundo plano)
brew services start ollama

# Verificar que Ollama está corriendo
ollama --version
```

### 4.4. Instalar la Extensión Continue

Busca e instala la extensión "Continue - open-source AI code agent" desde el Marketplace de VS Code.

## 5. Configuración


Ahora, configuraremos Continue para que use tu modelo local y el servidor MCP del tutor.

### 5.1. Descargar un Modelo Local

Necesitas un modelo que soporte *tool-calling*. `qwen3:8b` es una buena opción para tu hardware (Mac M4, 16 GB RAM).

```bash
# Descargar el modelo (puede tardar varios minutos)
ollama pull qwen3:8b
```

### 5.2. Configurar Continue

Crea la siguiente estructura de directorios y archivos dentro de la raíz de tu proyecto (`polski-tutor/`):

```
.continue/
├── mcpServers/
│   └── polski-tutor.yaml
├── models/
│   └── qwen3-local.yaml
└── rules/
    └── tutor-de-polaco.yaml
```

**Contenido de los archivos:**

#### `.continue/models/qwen3-local.yaml`

Este archivo define el modelo local que usaremos.

```yaml
name: qwen3-local
model: qwen3:8b
provider: ollama
roles:
  - chat
  - edit
capabilities:
  - tool_use
```

#### `.continue/mcpServers/polski-tutor.yaml`

Este archivo configura el servidor MCP del tutor.

```yaml
name: polski-tutor
type: stdio
# Rutas ABSOLUTAS a propósito. Continue no garantiza lanzar el proceso desde la
# raíz del repositorio, y con rutas relativas el servidor no arranca sin decir
# por qué: el modelo se queda sin herramientas y empieza a improvisar
# contenido, que es justo lo que este proyecto evita.
command: /Users/mauricioonoro/polski-tutor/.venv/bin/python
args:
  - /Users/mauricioonoro/polski-tutor/mcp/server.py
env:
  PYTHONUNBUFFERED: "1"
```

Comprueba que arranca **desde otro directorio**, que es como lo lanzará Continue:

```bash
cd /tmp && echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
  | /Users/mauricioonoro/polski-tutor/.venv/bin/python \
    /Users/mauricioonoro/polski-tutor/mcp/server.py
```

Debe listar **20 herramientas**.

#### `.continue/rules/tutor-de-polaco.yaml`

Este archivo define el comportamiento del asistente (el rol del tutor).

```yaml
name: tutor-de-polaco
alwaysApply: true
rule: |
  (contenido completo en .continue/rules/tutor-de-polaco.yaml)

  Veinte herramientas, en cinco grupos:
    empezar       diagnostico_inicial
    consulta      buscar_concepto, explicar_concepto, obtener_ejercicios,
                  cobertura_concepto
    progreso      corregir_respuesta, puntos_debiles, estado_alumno,
                  repasos_pendientes
    nivel         mi_nivel, tests_autenticos, test_autentico, registrar_test,
                  corregir_produccion, preparacion_b1
    pronunciación escuchar, comprobar_escucha, ejercicio_pronunciacion,
                  comprobar_pronunciacion, audio_de
```

### 5.3. Qué modelos locales sirven

El tutor **depende de que el modelo sepa invocar herramientas**. Sin eso no
consulta el grafo y se pone a inventar polaco, que es exactamente el fallo que
todo este proyecto trata de impedir.

Hay una configuración lista por modelo en `.continue/models/`:

| Fichero | Modelo | ¿Herramientas? | |
|---|---|---|---|
| `qwen3-local.yaml` | `qwen3:8b` | sí | el instalado; va JUSTO con 20 herramientas |
| `qwen3-14b.yaml` | `qwen3:14b` | sí | el más fiable con reglas largas |
| `llama3.1-8b.yaml` | `llama3.1:8b` | sí | alternativa ligera |
| `mistral-nemo.yaml` | `mistral-nemo:12b` | sí | equilibrio tamaño/obediencia |
| — | **`gemma3:12b`** | **NO** | sin config a propósito: **no sirve aquí** |

Para cambiar de modelo basta seleccionar otro en el desplegable de Continue.
Descarga solo el que vayas a usar: `ollama pull qwen3:14b`.

**Todas llevan `contextLength: 32768`, y no es un capricho.** Ollama usa 4.096
por defecto aunque el modelo declare 40.960, y solo las definiciones de las 20
herramientas más las reglas ocupan ~2.400 tokens; una respuesta de
`obtener_ejercicios` o `explicar_concepto` añade otros ~1.500. Al desbordar, lo
primero que se trunca son las herramientas: el modelo **olvida que las tiene y
empieza a inventar gramática polaca a media sesión**, sin dar ningún aviso.

Con 20 herramientas y un rol largo, un 8B va justo: tiende a responder de
memoria en vez de llamar. Si lo notas, sube a 14B o pídeselo explícitamente.

### 5.4. Configuración de VS Code para MCP (Opcional, para Copilot)

Si también quieres que Copilot (u otros agentes que usen MCP) pueda acceder al servidor, crea el archivo `.vscode/mcp.json` en la raíz del proyecto:

```json
{
  "servers": {
    "polski-tutor": {
      "type": "stdio",
      "command": "/Users/mauricioonoro/polski-tutor/.venv/bin/python",
      "args": ["/Users/mauricioonoro/polski-tutor/mcp/server.py"]
    }
  }
}
```

## 6. Uso en VS Code


Una vez configurado todo, puedes empezar a usar el tutor local:

1.  **Reinicia VS Code**: Ejecuta `Developer: Reload Window` desde la paleta de comandos (`Cmd+Shift+P`).
2.  **Selecciona el modelo local**: Abre el panel de **Continue** (icono en la barra lateral izquierda de VS Code). En la parte superior, selecciona el modelo **`qwen3-local`**.
3.  **Chatea con el tutor**: Escribe tus preguntas o peticiones en la ventana de chat de Continue. El asistente usará el modelo local y las herramientas MCP automáticamente.

Los ejemplos de qué pedirle están en la sección siguiente.

El modelo local invocará las herramientas del MCP (`buscar_concepto`, `obtener_ejercicios`, `corregir_respuesta`, etc.) para responderte. Tu progreso se guardará en la base de datos `polski.db` (capa L3).

## 7. Qué decirle a la IA


Todos estos prompts están **probados contra `qwen3:8b` en local** y disparan la
herramienta indicada. No hace falta nombrar las herramientas: basta pedir la
cosa en lenguaje normal.

| Le dices | Llama a |
|---|---|
| *Veamos qué sé de polaco* / *Quiero empezar* | `diagnostico_inicial` |
| *Dame 5 ejercicios para practicar el acusativo* | `obtener_ejercicios` |
| *Explícame el locativo* | `explicar_concepto` |
| *¿Cuáles son mis puntos débiles?* | `puntos_debiles` |
| *Quiero practicar* | `obtener_ejercicios` |
| *¿Qué tal voy?* | `estado_alumno` |
| *Enséñame el genitivo plural con ejercicios del libro* | `obtener_ejercicios` |
| *Ponme a prueba con la pronunciación de ś y sz* | `ejercicio_pronunciacion` |
| *¿El vocativo tiene material en el corpus?* | `cobertura_concepto` |
| *¿Qué vocabulario me toca repasar hoy?* | `repasos_pendientes` |
| *¿Cuál es mi nivel?* / *¿Ya soy A2?* | `mi_nivel` |
| *¿Qué tests auténticos me quedan?* | `tests_autenticos` |
| *Quiero hacer un test de verdad* | `test_autentico` |
| *He escrito esta redacción, corrígemela* | `corregir_produccion` |
| *¿Qué me falta para el B1?* | `preparacion_b1` |

### Nivel: lo que el sistema puede y no puede afirmar

```
Tú:  ¿Cuál es mi nivel?
IA:  [mi_nivel] → si nunca has hecho un test auténtico ni te han corregido
     una redacción, responde «sin evidencia suficiente» y te ofrece un test.
     No te da un porcentaje.
```

Eso es deliberado. El inventario de conceptos dominados es diagnóstico interno:
sirve para elegir qué practicar, pero **dominar el 80 % de los conceptos A2
aislados no es ser A2**. El nivel que el sistema afirma sale solo de (a) tests
auténticos de los autores y (b) producción corregida con rúbrica.

```
Tú:  Quiero hacer un test de verdad
IA:  [tests_autenticos → test_autentico] → te entrega los ítems sin las
     respuestas. Al terminar llama a registrar_test con la nota.
```

Quedan **7 tests** y son de un solo uso limpio: repetir uno ya hecho se marca
contaminado y no mueve el nivel. Gástalos en hitos, no cada semana.

**Para B1 no hay test cerrado.** El `zestaw egzaminacyjny` del examen estatal no
está en estos libros —sale solo en la publicidad de contraportada—, así que la
evidencia de B1 viene de redacción evaluada con `corregir_produccion`, que
exige rúbrica explícita para que la nota sea auditable.

### La primera sesión

```
Tú:  Veamos qué sé de polaco
IA:  [diagnostico_inicial] → ejercicios repartidos por nivel, de A1 a B1,
     de uno en uno. Cuando falles dos seguidos de un nivel, para: ya
     sabe dónde estás. Termina diciéndote qué reforzar.
```

Esto **sitúa, no certifica**: sale del pool de práctica. Los tests auténticos
son finitos y de un solo uso limpio, así que gastarlos en el primer contacto
tiraría la única medida fiable de nivel que existe.

### Sesión típica

```
Tú:  ¿Qué tal voy y qué debería practicar hoy?
IA:  [estado_alumno + puntos_debiles] → te dice el concepto más flojo
     y sus prerrequisitos, por si el problema real está más abajo

Tú:  Vale, dame 10 ejercicios de eso
IA:  [obtener_ejercicios] → ítems reales, citando libro, lekcja y página

Tú:  1. kawę  2. mleka  3. chleb
IA:  [corregir_respuesta ×3] → corrige contra la clave DEL LIBRO y
     registra el avance; te dice en qué peldaño aceptó cada una

Tú:  Ahora pronunciación de las nasales
IA:  [ejercicio_pronunciacion] → «di kąt (ángulo), se confunde con kat
     (verdugo); graba las dos y pásame las rutas»
```

### No hace falta el identificador exacto

El servidor resuelve el concepto por nombre, en español o en polaco:

- *«locativo»*, *«miejscownik»* y `gram.case.loc` llevan al mismo sitio.
- Si pides un prefijo (`gram.case.acc`) te da el concepto más general.
- Si pides pronunciación de algo que no es un contraste fonético, te devuelve
  la lista de los que hay en vez de un error seco.

Esto está en el servidor **a propósito**: los modelos locales pequeños mandan
el nombre en vez del id, y exigir el formato exacto los dejaba sin datos —
momento en el que se ponen a improvisar gramática polaca, que es justo lo que
este proyecto existe para impedir.

### Cómo saber si de verdad está usando el MCP

Es el fallo más importante de todos, porque **no da ningún error**: si el
servidor no está conectado, el modelo responde igual — leyendo los ficheros del
repositorio que Continue indexa— y produce informes que suenan perfectamente
creíbles y son falsos por completo.

Un caso real, en la primera sesión de un alumno que no había respondido nada:

> *«Basado en tu progreso actual (45% de ejercicios completados)… Has dominado
> el 78% de los conceptos básicos… Tu desempeño en tests de pronunciación
> alcanza el 82%… Usa el modo "contraste auditivo"…»*

Todo inventado. Los números salían del README —donde describen el **corpus**, no
al alumno— y el «modo contraste auditivo» no existe.

**Tres señales de que NO está consultando:**

1. Da porcentajes de progreso sin que hayas hecho ningún ejercicio.
2. No cita libro, lekcja y página. **La cita es la prueba** de que los datos
   son reales.
3. Menciona «modos» o funciones que no están entre las doce herramientas.

**Comprobación en 10 segundos.** Pregúntale *«¿qué tal voy?»* en una sesión
nueva. La respuesta correcta es que es tu primera sesión y no hay progreso.
Cualquier porcentaje es invención.

**Si está inventando:**

```bash
# 1. ¿arranca el servidor?  Debe listar 12 herramientas
cd /tmp && echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
  | /Users/mauricioonoro/polski-tutor/.venv/bin/python \
    /Users/mauricioonoro/polski-tutor/mcp/server.py

# 2. recarga VS Code tras cualquier cambio en .continue/
#    Cmd+Shift+P -> Developer: Reload Window
```

En el panel de Continue, el servidor `polski-tutor` debe aparecer conectado y
con sus herramientas listadas. Si no está, el tutor no consulta nada.

### Si el modelo responde sin usar las herramientas

Le pasa a los modelos de 8B con un rol largo. Tres remedios, de menor a mayor:

1. Pídeselo explícito: *«usa obtener_ejercicios para el acusativo»*.
2. Empieza la sesión con *«¿qué tal voy?»*: una llamada exitosa lo encarrila.
3. Sube a `qwen3:14b`, que obedece mejor instrucciones largas.

Si responde en polaco inventado y **sin citar libro ni página**, no está usando
el MCP. La cita es la señal de que los datos son reales.

## 8. Uso desde la terminal (sin IA)


```bash
polski estado                     # resumen y nivel estimado
polski explicar gram.case.loc     # qué es, de qué depende, dónde estudiarlo
polski practicar --concepto ...   # sesión de práctica con corrección
polski debiles                    # qué reforzar, con sus prerrequisitos
polski test-autentico             # examen del pool reservado
```

Como MCP: apunta tu cliente a `mcp/server.py`. Doce herramientas, sin
dependencias externas.

## 9. Decisiones que conviene conocer antes de tocar nada


**Tres capas separadas por ciclo de vida.** L1 es la ontología curada a mano;
L2 el contenido extraído, que se puede borrar y regenerar entero; L3 tu
progreso. Estudiar solo escribe en L3, así que reiniciarlo no deshace nada del
grafo — verificado. Cada persona puede tener su propio fichero de progreso
(`tutor/alumno.py`) compartiendo un grafo que cuesta horas construir.

**La gramática la etiqueta Morfeusz, no el LLM.** Si la respuesta es `kawę`, el
analizador morfológico devuelve `kawa / subst:sg:acc:f` con certeza. El 54 % de
las formas son ambiguas (`kawy` es genitivo singular *y* nominativo plural), así
que Stanza desambigua por contexto y resuelve el 46 % de los casos; el resto
queda marcado como pendiente en vez de elegirse al azar.

**Las respuestas vienen del libro.** Todas salen del `Klucz do ćwiczeń`. Las de
los manuales están en su libro del profesor, no en el propio manual — se
verificó por coincidencia de contenido, no por numeración. El tutor nunca
inventa una respuesta: si no la tiene, lo dice.

**Práctica y evaluación no se mezclan.** Los `testy osiągnięć` están marcados
`held_out` y las vistas los excluyen del pool de práctica. Si el tutor entrena
con lo mismo con que evalúa, el nivel mide memoria y no competencia.

**El nivel se ancla a evidencia.** Dominar conceptos sueltos no es tener un
nivel: el MCEV se define por lo que uno sabe hacer. Sin tests auténticos ni
producción evaluada, `nivel_reportado()` devuelve "sin evidencia suficiente" —
esa negativa es una funcionalidad, no una carencia.

**Los agujeros se declaran.** 30 conceptos no tienen material, todos temas
léxicos sueltos que el índice nombra pero el texto no desarrolla. Ninguno es
gramatical.
`v_concept_coverage` los expone para que el tutor lo diga en vez de improvisar.

## 10. Pronunciación


Dos mitades, ambas funcionando y validadas sobre grabación humana real:

**Percepción** (`tutor/percepcion.py`) — se reproduce una palabra y se elige
cuál de las dos era. Medición exacta: el sistema generó el audio, así que
conoce la respuesta y no interviene ningún reconocedor que pueda dar un falso
aprobado. 20 pares mínimos, todos verificados como audibles.

**Producción** (`tutor/pronunciacion.py`) — el alumno graba las DOS palabras de
un par y se comprueba si las pronuncia distinto. **20/20 sobre grabación real.**

Dos cosas que no funcionan y están medidas, para que nadie las reintente:

| Enfoque | Acierto | Por qué falla |
|---|---|---|
| Transcribir con Whisper y comparar | 40 % | Devuelve texto fluido: "arregla" el error y lo oculta |
| Comparar la voz del alumno con TTS | 52 % | Voz humana y sintética difieren tanto que ambas distancias empatan |
| **Comparar las dos grabaciones entre sí** | **100 %** | Mismo hablante y condiciones: la única diferencia es la que interesa |

Y es además la pregunta correcta: lo que importa no es si una palabra suelta se
acerca a un ideal abstracto, sino si el alumno **produce el contraste**. Si dice
`kąt` y `kat` igual, no lo tiene, aunque ambas suenen razonables por separado.

### Por qué un TTS mejor no arreglaría el 52 %

La sospecha natural es que la voz sintética no habla suficiente polaco. Medido,
no es eso — distancia fonémica media sobre 10 pares:

| Comparación | Distancia | Qué es |
|---|---|---|
| TTS(`kąt`) vs TTS(`kat`) | 2,3 | señal: el contraste |
| humano(`kąt`) vs humano(`kat`) | 2,2 | señal: el contraste |
| humano(`kąt`) vs TTS(`kąt`) | **2,7** | ruido: misma palabra, distinta voz |

**El ruido supera a la señal.** La misma palabra dicha por dos hablantes difiere
más que dos palabras distintas dichas por el mismo. Y el TTS separa los pares
tan bien como el humano (2,3 frente a 2,2): su pronunciación no es el problema.

El obstáculo es la variabilidad entre hablantes, no la calidad de la síntesis,
así que cambiar de voz —incluso a un nativo grabado en estudio— no lo resuelve.
Se arreglaría con representaciones normalizadas por hablante o alineamiento
forzado con scoring GOP, pero eso es mucha complejidad para llegar donde el
método de contraste ya llega.

## 11. Verificación


Los scripts de `spike/` son la suite de comprobación; cada uno mide una parte
con números reales sobre el corpus:

```bash
python3 spike/check_layout.py         # reconstrucción de layout
python3 spike/check_morph.py          # cobertura morfológica (99,3 %)
python3 spike/check_disambiguate.py   # desambiguación contextual
python3 spike/check_exercises.py      # segmentación vs. clave del libro
python3 spike/check_piloto.py         # calidad de la pasada LLM
```

## 12. Reconstruir el corpus desde cero


```bash
uv venv && source .venv/bin/activate
uv pip install morfeusz2 stanza transformers soundfile phonemizer
python3 graph/cargar_todo.py        # construye polski.db
python3 ingest/run_lote.py          # pasada LLM sobre el corpus (~2,5 h)
python3 -c "from graph import cargar_llm; cargar_llm.cargar()"
```

Los PDFs viven en iCloud y se tratan como **solo lectura**; `config/books.json`
los referencia por ruta. El corpus extraído no se versiona: es material con
copyright y se regenera desde los PDFs.

## 13. Mantenimiento


-   **Cambiar de modelo**: Si deseas probar otro modelo local (ej. `qwen3:14b` o `mistral-nemo:12b`), solo necesitas descargarlo con `ollama pull <nombre_modelo>` y actualizar la línea `model: qwen3:8b` en `.continue/models/qwen3-local.yaml`.
-   **Reinicio del progreso**: Si necesitas resetear tu avance en el tutor (ej. para empezar de cero), ejecuta el siguiente comando en la terminal del proyecto:
    ```bash
    .venv/bin/python -c "from tutor import alumno; import sqlite3; con = sqlite3.connect('polski.db'); alumno.reiniciar(con)"
    ```
    Esto borrará los datos de la capa L3 (`intentos`, `dominio_concepto`, etc.) pero conservará el grafo principal del proyecto.

TDQS

B3.1/5.0

Scored across 13 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: listening task vs listening check, pronunciation task vs pronunciation check, concept lookup vs explanation vs practice, and progress/weak-point tracking. The complementary pairs are related but not overlapping.

Naming Consistency3/5

Names mix verb-first patterns (escuchar, comprobar_escucha, buscar_concepto) with noun-first patterns (diagnostico_inicial, ejercicio_pronunciacion, puntos_debiles, cobertura_concepto, estado_alumno), plus an odd 'audio_de'. They are descriptive but not uniform.

Tool Count5/5

13 tools is within the ideal 3-15 range for a language tutor. Each tool covers a distinct aspect of learning or assessment, and none feel redundant or missing.

Completeness5/5

The surface covers the full learning loop: diagnostic placement, concept practice, listening and pronunciation exercises, response correction, progress tracking, weak-point analysis, and corpus coverage. There are no obvious dead ends or missing core operations.

Maintenance

ActivityStale
ResponsivenessNo issues