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.

En las fases anteriores construimos un chatbot capaz de:
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.
Al finalizar esta fase, nuestro chatbot podrá:
Antes de escribir código, debemos decidir cómo almacenar información.
Opciones posibles:
¿Por qué SQLite?
Elegimos SQLite porque:
| Herramienta | Uso |
|---|---|
| SQLite | Base de datos local |
| SQLAlchemy | ORM para manejar la base de datos |
| Pydantic | Validación de datos |
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
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?
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)
Separar estas responsabilidades es una práctica y facilita el mantenimiento y la escalabilidad del sistema.
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.
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.
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í:
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.
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.
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:
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:
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:
user_id)Esta información se guarda en una tabla llamada messages.
chatbot.dbmessagesEsta es la forma más sencilla y visual de comprobar que el historial se está guardando correctamente.
sqlite3 chatbot.db
.tables
SELECT * FROM messages;
Ahora el chatbot:
Cuando un usuario envía un mensaje:
Message.Gracias a esto, el bot ya no olvida lo que se envia y puede empezar a comportarse como un profesor que recuerda el progreso.
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í