Construye un Profesor de Inglés Personal con Python - Fase 3

Aprende a guardar historial y progreso de usuarios en un chatbot en Python usando FastAPI, SQLAlchemy y SQLite. Fase 3 del profesor de inglés local.

Construye un Profesor de Inglés Personal con Python - Fase 3

En las fases anteriores construimos un chatbot capaz de:

  • Recibir mensajes
  • Corregir frases en inglés de forma local usando LanguageTool

Sin embargo, nuestro bot todavía tiene una gran limitación: no recuerda nada.

Cada mensaje es independiente. Si cerramos el programa, todo se pierde.

En esta fase vamos a dar un paso clave para convertir el bot en un profesor real: 👉 guardar historial y progreso del usuario.


📚 Tabla de Contenidos



1️⃣ Objetivo de esta fase

Al finalizar esta fase, nuestro chatbot podrá:

  • Identificar al usuario
  • Guardar cada frase enviada
  • Guardar la corrección realizada
  • Mantener un historial local y persistente
  • Preparar la base para personalización e IA real


2️⃣ Decisión técnica: ¿cómo guardar datos?

Antes de escribir código, debemos decidir cómo almacenar información.

Opciones posibles:

  • Guardar datos en variables ❌ (se pierden al reiniciar)
  • Usar archivos JSON ❌ (difícil de escalar)
  • Usar una base de datos local ✅

¿Por qué SQLite?

Elegimos SQLite porque:

  • Es 100% local
  • No requiere servidor
  • Viene incluida con Python
  • Es muy usada en proyectos reales
  • Permite migrar fácilmente a PostgreSQL o MySQL

3️⃣ Herramientas nuevas de esta fase

HerramientaUso
SQLiteBase de datos local
SQLAlchemyORM para manejar la base de datos
PydanticValidación de datos

4️⃣ Actualizar requirements.txt

Añadimos las nuevas librerías al archivo requirements.txt:

fastapi==0.127.1
uvicorn==0.39.0
language-tool-python==2.9.5
sqlalchemy==2.0.4

Instalamos todo con:

pip install -r requirements.txt

5️⃣ Nueva estructura del proyecto

A medida que el proyecto crece, es importante organizar el código para no mezclar responsabilidades.

En lugar de tener todo en un solo archivo, vamos a separar cada parte según su función:

englishTutor/
├── app/
   ├── __init__.py
   ├── main.py                  # Punto de entrada FastAPI 
   ├── database/
      ├── __init__.py
      ├── base.py              # Declarative Base
      ├── session.py           # engine + SessionLocal
      └── crud/
          ├── __init__.py
          └── messages_crud.py
   ├── models/
      ├── __init__.py
      └── message.py
   ├── schemas/
      ├── __init__.py
      └── message_schemas.py
   ├── services/
      ├── __init__.py
      └── grammar_service.py
   └── routers/
       ├── __init__.py
       └── messages_router.py
└── requirements.txt

¿Por qué esta separación?

  • main.py → Arranca la app, Lifespan moderno y registra routers.
  • database/ → Contiene la configuración de la base de datos y los CRUDs (repositorios).
  • models/ → Define las entidades (tablas de SQLAlchemy).
  • schemas/ → Define los DTOs (Pydantic), lo que la API recibe y devuelve.
  • services/ → Contiene la lógica de negocio (LanguageTool, procesamiento, etc.).
  • routers/ → Contiene los endpoints de FastAPI.

Esta estructura es muy común en proyectos profesionales con FastAPI y permite que el proyecto escale sin volverse caótico.

🧠 Schemas vs Models (concepto clave)

  • Schemas (Pydantic): definen cómo se comunican los clientes con la API.
  • Models (SQLAlchemy): definen cómo se almacenan los datos en la base de datos.

Separar estas responsabilidades es una práctica y facilita el mantenimiento y la escalabilidad del sistema.

6️⃣ Cambios en el codigo

6.1: Paso 1: Configurar la base de datos

app/database/base.py

from sqlalchemy.orm import declarative_base

Base = declarative_base()

app/database/session.py

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "sqlite:///./chatbot.db"  # Ruta de la base de datos

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False}  # Necesario para SQLite + multithreading
)

SessionLocal = sessionmaker(
    autocommit=False,
    autoflush=False,
    bind=engine
)  # Fábrica de sesiones para interactuar con la DB

Qué hace: centraliza toda la configuración de la base de datos, crea sesiones reutilizables y evita que tengas que repetir la conexión en cada archivo.

6.2: Definir el modelo de historial

app/models/message.py

from sqlalchemy import Column, Integer, String
from app.database.base import Base

class MessageHistory(Base):
    __tablename__ = "messages"  # Nombre de la tabla

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(String, index=True)
    original_text = Column(String)
    corrected_text = Column(String)

Qué hace: define la estructura de la tabla messages en la base de datos. Cada instancia de MessageHistory representa un registro de un mensaje.

6.3: Esquemas de entrada y salida

app/schemas/message_schemas.py

from pydantic import BaseModel, Field

class Message(BaseModel):
    user_id: str
    text: str = Field(..., min_length=1)  # Valida que no esté vacío

class MessageResponse(BaseModel):
    original: str
    corrected: str
    message: str  # Feedback al usuario

class MessageHistoryResponse(BaseModel):
    original: str
    corrected: str

📌 Aquí:

  • Válida datos que entran a la API y define la forma de la respuesta.
  • Entrada: Message
  • Salida: MessageResponse o MessageHistoryResponse

6.4 Servicio de corrección (Lógica de negocio)

app/services/grammar_service.py

import language_tool_python
from fastapi.concurrency import run_in_threadpool

class LanguageToolService:
    def __init__(self):
        self.tool = language_tool_python.LanguageTool("en-US")  # Solo una instancia

    async def check_text(self, text: str) -> tuple[str, str]:
        matches = await run_in_threadpool(self.tool.check, text)  # Async-safe
        if not matches:
            return text, "✅ Your sentence is correct!"
        corrected = language_tool_python.utils.correct(text, matches)
        return corrected, "❌ We corrected your sentence."

Qué hace: encapsula la lógica de corrección de texto.

  • No bloquea el loop de FastAPI
  • Se puede reutilizar en cualquier endpoint

6.5: CRUD (Repository Pattern)

app/database/crud/messages_crud.py

from sqlalchemy.orm import Session
from app.models.message import MessageHistory

def create_message(db: Session, user_id: str, original_text: str, corrected_text: str):
    message = MessageHistory(
        user_id=user_id,
        original_text=original_text,
        corrected_text=corrected_text
    )
    db.add(message)
    db.commit()
    db.refresh(message)  # Devuelve la instancia con el id generado
    return message

def get_history_by_user(db: Session, user_id: str):
    return db.query(MessageHistory).filter(MessageHistory.user_id == user_id).all()

Qué hace: funciones para interactuar con la DB sin exponer la lógica interna a los endpoints.

6.6: Routers (Controller / API Layer)

app/routers/messages_router.py

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from typing import List
from app.schemas.message_schemas import Message, MessageResponse, MessageHistoryResponse
from app.services.grammar_service import LanguageToolService
from app.database.crud import messages_crud
from app.database.session import SessionLocal

router = APIRouter()
grammar_service = LanguageToolService()

# Dependency
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

@router.post("/sendMessage", response_model=MessageResponse)
async def send_message(msg: Message, db: Session = Depends(get_db)):
    corrected, feedback = await grammar_service.check_text(msg.text)
    messages_crud.create_message(db, msg.user_id, msg.text, corrected)
    return MessageResponse(original=msg.text, corrected=corrected, message=feedback)

@router.get("/history/{user_id}", response_model=List[MessageHistoryResponse])
def get_history(user_id: str, db: Session = Depends(get_db)):
    messages = messages_crud.get_history_by_user(db, user_id)
    return [
        MessageHistoryResponse(original=m.original_text, corrected=m.corrected_text)
        for m in messages
    ]

Qué hace:

  • Expone los endpoints /sendMessage y /history/{user_id}
  • Llama al servicio para lógica de corrección
  • Llama al CRUD para persistencia
  • Devuelve objetos validados según los schemas

6.7 Punto de entrada (main.py)

app/main.py

from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.database.base import Base
from app.database.session import engine
from app.routers import messages_router

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup
    Base.metadata.create_all(bind=engine)  # Crea tablas si no existen
    yield
    # Shutdown opcional

app = FastAPI(lifespan=lifespan)
app.include_router(messages_router.router)  # Registra endpoints

Qué hace:

  • Inicializa la app y las tablas en DB
  • Registra todos los routers
  • Aplica Lifespan moderno en lugar de @on_event(“startup”)

7️⃣ Probar los endpoints y ver los resultados

Endpoint para enviar un mensaje:

{
  "user_id": "amoelcodigo",
  "text": "I has a apple"
}

Respuesta:

{
  "original": "I has a apple",
  "corrected": "I have an apple",
  "message": "❌ We corrected your sentence."
}

Endpoint para ver el historial de un usuario:

import requests

url = "http://127.0.0.1:8000/history/amoelcodigo"
response = requests.get(url)
print(response.json())

Otra manera también es probando por postman.

Respuesta:

[
  {"original": "I has a apple", "corrected": "I have an apple"},
  {"original": "She go to school", "corrected": "She goes to school"}
]

Cada vez que enviamos un mensaje al endpoint, el chatbot guarda la información en una base de datos local llamada SQLite.

En concreto, se crea automáticamente un archivo:

chatbot.db

Este archivo contiene toda la información del historial del chatbot.

📋 ¿Qué información se almacena?

En esta fase guardamos:

  • El identificador del usuario (user_id)
  • El texto original enviado
  • El texto corregido por el sistema

Esta información se guarda en una tabla llamada messages.

🧪 Cómo ver los datos guardados en SQLite

Opción 1: Usar DB Browser for SQLite (recomendado)

  1. Descarga DB Browser for SQLite
  2. Abre el archivo chatbot.db
  3. Selecciona la tabla messages
  4. Verás todas las frases enviadas al chatbot y sus correcciones

Esta es la forma más sencilla y visual de comprobar que el historial se está guardando correctamente.

Opción 2: Ver los datos desde la consola

sqlite3 chatbot.db
.tables
SELECT * FROM messages;

✅ Resultado de la Fase 3

Ahora el chatbot:

  • Guarda historial persistente
  • Recuerda al usuario
  • Permite analizar errores y progreso
  • Tiene una base sólida para personalización e inteligencia artificial
  • A partir de aquí, el bot deja de ser un simple corrector: ahora recuerda al usuario y su historial, lo que sienta las bases para comportarse como un profesor real en fases posteriores.

🧠 Cómo funciona internamente

Cuando un usuario envía un mensaje:

  1. FastAPI valida los datos usando el schema Message.
  2. LanguageTool corrige la frase si es necesario.
  3. El mensaje original y la corrección se guardan en la base de datos SQLite.
  4. El historial queda disponible para futuras consultas y para personalizar la enseñanza en fases posteriores.

Gracias a esto, el bot ya no olvida lo que se envia y puede empezar a comportarse como un profesor que recuerda el progreso.

🚀 ¿Qué sigue?

En la Fase 4 comenzaremos a integrar inteligencia artificial local usando GPT4All, de manera que el bot pueda generar respuestas personalizadas y ofrecer retroalimentación más avanzada.

Si quieres ir a la fase 4 haz click aquí