Ollama + Python y Node.js: Crea tu propio chat de IA local

Actualizado en marzo 2026: Este artículo ha sido revisado con las versiones más recientes de las librerías utilizadas: Ollama Python SDK v0.6.1, Ollama JS SDK v0.6.3, FastAPI v0.115 y Tailwind CSS v4.
Antes de empezar
En el post anterior instalamos Ollama y aprendimos a correr un LLM en nuestra máquina. Si todavía no lo leíste, te recomiendo empezar por ahí — en 10 minutos tendrás todo listo.
Hoy vamos a dar el siguiente paso: construir una aplicación web tipo ChatGPT que se conecta a tu LLM local. Vamos a hacerlo en dos sabores:
- Python con FastAPI + la librería oficial de Ollama
- Node.js con Express + la librería oficial de Ollama
Ambas versiones usan Tailwind CSS v4 para el frontend y streaming en tiempo real (Server-Sent Events) para que veas las respuestas generándose token por token, igualito a como funciona ChatGPT.
Pre-requisitos
Antes de empezar, asegúrate de tener instalado:
| Herramienta | Versión mínima | Para qué |
|---|---|---|
| Ollama | v0.18+ | Correr el LLM local |
| Python | 3.13+ | Demo en Python |
| Node.js | 22+ | Demo en Node.js |
| Git | 2.x | Clonar los repos |
Y necesitas tener al menos un modelo descargado en Ollama:
# Si todavía no lo tienes
ollama run llama3.1http://localhost:11434 en tu navegador. Deberías ver “Ollama is running”.Arquitectura de la app
Antes de meternos al código, veamos cómo funciona esto:
- El usuario escribe un mensaje en el browser.
- El backend recibe el mensaje y lo envía a Ollama via su API local.
- Ollama genera la respuesta token por token (streaming).
- El backend retransmite cada token al browser usando Server-Sent Events (SSE).
- El browser muestra cada token en tiempo real, como ChatGPT.
Demo 1: Python + FastAPI
Clonar el repositorio
git clone https://github.com/pescarcena/blog-pescarcena-code.git
cd blog-pescarcena-code/ollama-local-app-01Estructura del proyecto
ollama-local-app-01/
├── app.py # Backend FastAPI
├── requirements.txt # Dependencias Python
├── Dockerfile # Para correr con Docker
├── templates/
│ └── index.html # Frontend con Tailwind v4
└── static/ # Archivos estáticosInstalar dependencias y correr
# Crear entorno virtual
python3 -m venv venv
source venv/bin/activate # En Windows: venv\Scripts\activate
# Instalar dependencias
pip install -r requirements.txt
# Correr la app
uvicorn app:app --reloadLa app estará disponible en http://localhost:8000.
El backend: app.py
Veamos las partes clave del código. El corazón es el endpoint /api/chat:
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from ollama import chat
import json
app = FastAPI()
class ChatRequest(BaseModel):
message: str
model: str = "llama3.1"
history: list = []
@app.post("/api/chat")
async def chat_endpoint(req: ChatRequest):
messages = [{"role": m["role"], "content": m["content"]} for m in req.history]
messages.append({"role": "user", "content": req.message})
def generate():
stream = chat(
model=req.model,
messages=messages,
stream=True,
)
for chunk in stream:
content = chunk["message"]["content"]
yield f"data: {json.dumps({'content': content})}\n\n"
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")¿Qué está pasando aquí?
ChatRequest: recibe el mensaje del usuario, el modelo a usar y el historial de la conversación.chat(stream=True): le dice a Ollama que nos envíe la respuesta token por token en vez de esperar a que termine.StreamingResponse: FastAPI retransmite cada token al browser como un Server-Sent Event.history: mantiene el contexto de la conversación para que el modelo “recuerde” lo que hablaron antes.
También tenemos un endpoint para listar los modelos disponibles:
@app.get("/api/models")
async def list_models():
import ollama
models = ollama.list()
return {"models": [m.model for m in models.models]}El frontend: Tailwind CSS v4
El frontend es un solo archivo HTML que usa Tailwind v4 via CDN. Esto lo hace súper ligero — sin build step, sin webpack, sin nada:
<head>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>La magia del streaming en el frontend está en leer la respuesta como un stream:
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, model: modelSelect.value, history })
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
const lines = text.split('\n');
for (const line of lines) {
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
const data = JSON.parse(line.slice(6));
fullResponse += data.content;
aiBubble.textContent = fullResponse;
}
}
}Esto lee los chunks de datos conforme llegan y actualiza el DOM en tiempo real. El resultado es una experiencia fluida donde ves las palabras aparecer una por una.
Demo 2: Node.js + Express
Clonar el repositorio
git clone https://github.com/pescarcena/blog-pescarcena-code.git
cd blog-pescarcena-code/ollama-local-app-02Estructura del proyecto
ollama-local-app-02/
├── server.js # Backend Express
├── package.json # Dependencias Node.js
├── Dockerfile # Para correr con Docker
└── public/
└── index.html # Frontend con Tailwind v4Instalar dependencias y correr
# Instalar dependencias
npm install
# Correr la app
npm start
# O en modo desarrollo (auto-reload)
npm run devLa app estará disponible en http://localhost:3000.
El backend: server.js
La versión de Node.js es muy similar en estructura. Acá está el endpoint principal:
import express from 'express';
import { Ollama } from 'ollama';
const app = express();
const ollama = new Ollama();
app.post('/api/chat', async (req, res) => {
const { message, model = 'llama3.1', history = [] } = req.body;
const messages = history.map(m => ({ role: m.role, content: m.content }));
messages.push({ role: 'user', content: message });
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const stream = await ollama.chat({ model, messages, stream: true });
for await (const chunk of stream) {
const content = chunk.message.content;
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
res.write('data: [DONE]\n\n');
res.end();
});La API de la librería ollama para Node.js es prácticamente idéntica a la de Python:
ollama.chat({ model, messages, stream: true })retorna un async iterable.- Usamos
for await...ofpara iterar sobre los chunks. - Cada chunk se envía al browser como SSE.
El frontend
El frontend es idéntico en funcionalidad al de Python (mismo HTML/JS). La única diferencia visual es el color del tema: indigo para Python y esmeralda para Node.js, para que los distingas fácilmente.
Correr con Docker
Ambos proyectos incluyen un Dockerfile para que puedas correrlos sin instalar dependencias locales.
Python
cd ollama-local-app-01
# Construir la imagen
docker build -t ollama-chat-python .
# Correr el contenedor
docker run -p 8000:8000 --add-host=host.docker.internal:host-gateway ollama-chat-pythonNode.js
cd ollama-local-app-02
# Construir la imagen
docker build -t ollama-chat-nodejs .
# Correr el contenedor
docker run -p 3000:3000 --add-host=host.docker.internal:host-gateway ollama-chat-nodejs--add-host=host.docker.internal:host-gateway es necesario en Linux para que el contenedor pueda acceder a Ollama que corre en tu host. En macOS y Windows con Docker Desktop, host.docker.internal ya funciona automáticamente.¿Y esto cómo funcionaría en Kubernetes?
Si estás pensando “oye, ¿puedo meter esto en un cluster de Kubernetes?”, la respuesta es absolutamente sí. De hecho, es un caso de uso muy interesante.
Imagina esta arquitectura:
En Kubernetes podrías:
- Escalar la app de chat horizontalmente con HPA según la demanda.
- Dedicar nodos con GPU para correr Ollama y servir el modelo.
- Usar un Service interno para que la app de chat se comunique con Ollama sin exponer el LLM al exterior.
- Configurar resource limits para que el modelo no se coma toda la memoria del nodo.
- Implementar health checks para reiniciar automáticamente si Ollama se queda sin responder.
En un próximo post vamos a abordar exactamente esto: cómo desplegar un LLM con Ollama sobre Kubernetes, incluyendo configuración de GPU, manifiestos, y buenas prácticas para correr modelos en un cluster.
Resumen
| Python (FastAPI) | Node.js (Express) | |
|---|---|---|
| Repositorio | ollama-local-app-01 | ollama-local-app-02 |
| Ollama SDK | Python v0.6.1 | JS v0.6.3 |
| Puerto | 8000 | 3000 |
| Streaming | StreamingResponse + SSE | res.write() + SSE |
| Frontend | Tailwind v4 CDN | Tailwind v4 CDN |
Ambas versiones hacen exactamente lo mismo: te dan un chat funcional con tu LLM local, con streaming en tiempo real y selector de modelos. Elige la que más se ajuste a tu stack.