|
| 1 | +--- |
| 2 | +blogpost: true |
| 3 | +date: Mar 12, 2025 |
| 4 | +author: hellhound |
| 5 | +location: Lima, Perú |
| 6 | +category: Tutorial |
| 7 | +tags: pyodide, openai, gpt, httpx, python |
| 8 | +language: Español |
| 9 | +--- |
| 10 | +# Creación de una Aplicación de Chat Potenciada por Pyodide y GPT-3.5 Turbo: Una Prueba de Concepto |
| 11 | + |
| 12 | +{ align=center width=400px } |
| 13 | + |
| 14 | +Construir una aplicación basada en la web que aproveche tanto el entorno de |
| 15 | +Python como el modelo de lenguaje GPT-3.5 Turbo de OpenAI puede ser una empresa |
| 16 | +emocionante. Este artículo explica la creación de una aplicación de chat como |
| 17 | +prueba de concepto utilizando Pyodide, una herramienta que permite ejecutar |
| 18 | +Python en el navegador web, e integrarla con GPT-3.5 Turbo para simular un |
| 19 | +agente conversacional inteligente. |
| 20 | + |
| 21 | +## El Panorama de la Integración entre Pyodide y GPT-3.5 Turbo |
| 22 | + |
| 23 | +Con la capacidad de ejecutar Python directamente en los navegadores web, Pyodide |
| 24 | +ofrece una oportunidad emocionante para llevar las capacidades poderosas de las |
| 25 | +bibliotecas basadas en Python directamente a las aplicaciones del lado del |
| 26 | +cliente. Esto incluye aplicaciones que pueden beneficiarse de estar cerca de los |
| 27 | +usuarios, como herramientas interactivas y tableros de visualización de datos. |
| 28 | + |
| 29 | +Esta prueba de concepto integra Pyodide con el modelo GPT-3.5 Turbo de OpenAI, |
| 30 | +que proporciona la capacidad de simular una conversación similar a la humana. A |
| 31 | +continuación, te guiaré a través de cada componente de esta aplicación, |
| 32 | +explicando su funcionalidad, técnicas de integración y la razón detrás de |
| 33 | +cada elección. |
| 34 | + |
| 35 | +## Desglose de Componentes |
| 36 | + |
| 37 | +La aplicación se compone de varios archivos clave y tecnologías, incluyendo |
| 38 | +HTML, JavaScript y Python integrados a través de Pyodide. Vamos a desglosar |
| 39 | +estos componentes paso a paso. |
| 40 | + |
| 41 | +### HTML: Estructuración de la Interfaz |
| 42 | + |
| 43 | +El archivo HTML configura la interfaz básica de usuario para la aplicación de |
| 44 | +chat: |
| 45 | + |
| 46 | +```html |
| 47 | +<!DOCTYPE html> |
| 48 | +<html lang="en"> |
| 49 | +<head> |
| 50 | + <meta charset="UTF-8"> |
| 51 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 52 | + <title>Pyodide Chat App</title> |
| 53 | + <style> |
| 54 | + body { |
| 55 | + font-family: Arial, sans-serif; |
| 56 | + max-width: 600px; |
| 57 | + margin: auto; |
| 58 | + padding: 20px; |
| 59 | + } |
| 60 | + #chatbox { |
| 61 | + border: 1px solid #ccc; |
| 62 | + height: 300px; |
| 63 | + overflow-y: scroll; |
| 64 | + padding: 10px; |
| 65 | + margin-bottom: 10px; |
| 66 | + } |
| 67 | + #user-input { |
| 68 | + width: calc(100% - 70px); |
| 69 | + } |
| 70 | + #send-button { |
| 71 | + width: 60px; |
| 72 | + } |
| 73 | + </style> |
| 74 | +</head> |
| 75 | +<body> |
| 76 | + |
| 77 | +<!-- Pyodide setup logic will insert content here --> |
| 78 | +<script src="https://cdn.jsdelivr.net/pyodide/v0.27.3/full/pyodide.js"></script> |
| 79 | +<script src="python.js"></script> |
| 80 | +</body> |
| 81 | +</html> |
| 82 | +``` |
| 83 | + |
| 84 | +Este diseño HTML crea una interfaz básica con un cuadro de chat y un control de |
| 85 | +entrada de usuario, envuelto en CSS simple para estilizar la apariencia. La |
| 86 | +configuración de Pyodide es gestionada por un archivo JavaScript que |
| 87 | +exploraremos a continuación. |
| 88 | + |
| 89 | +### JavaScript: Inicialización de Pyodide y Conexión del Frontend con el Backend |
| 90 | + |
| 91 | +Se emplea JavaScript para configurar el entorno Pyodide y conectar la interfaz |
| 92 | +de usuario del frontend con el backend de Python. Aquí está el archivo de |
| 93 | +inicialización de JavaScript: |
| 94 | + |
| 95 | +#### `python.js` |
| 96 | + |
| 97 | +```javascript |
| 98 | +async function setupPyodide() { |
| 99 | + const pyodide = await loadPyodide(); |
| 100 | + await pyodide.loadPackage("micropip"); |
| 101 | + |
| 102 | + // JavaScript functions to register with the Python environment |
| 103 | + const jsModule = { |
| 104 | + async displayResponse(response) { |
| 105 | + const chatbox = document.getElementById("chatbox"); |
| 106 | + chatbox.innerHTML += `<div><strong>AI:</strong> ${response}</div>`; |
| 107 | + } |
| 108 | + }; |
| 109 | + |
| 110 | + pyodide.registerJsModule("js_module", jsModule); |
| 111 | + |
| 112 | + await pyodide.runPythonAsync(` |
| 113 | + import micropip |
| 114 | + import os |
| 115 | + from pyodide.http import pyfetch |
| 116 | +
|
| 117 | + response = await pyfetch("app.tar.gz") |
| 118 | + await response.unpack_archive() |
| 119 | +
|
| 120 | + await micropip.install('https://raw.githubusercontent.com/psymbio/pyodide_wheels/main/multidict/multidict-4.7.6-py3-none-any.whl', keep_going=True) |
| 121 | + await micropip.install('https://raw.githubusercontent.com/psymbio/pyodide_wheels/main/frozenlist/frozenlist-1.4.0-py3-none-any.whl', keep_going=True) |
| 122 | + await micropip.install('https://raw.githubusercontent.com/psymbio/pyodide_wheels/main/aiohttp/aiohttp-4.0.0a2.dev0-py3-none-any.whl', keep_going=True) |
| 123 | + await micropip.install('https://raw.githubusercontent.com/psymbio/pyodide_wheels/main/openai/openai-1.3.7-py3-none-any.whl', keep_going=True) |
| 124 | + await micropip.install('https://raw.githubusercontent.com/psymbio/pyodide_wheels/main/urllib3/urllib3-2.1.0-py3-none-any.whl', keep_going=True) |
| 125 | + await micropip.install("ssl") |
| 126 | + import ssl |
| 127 | + await micropip.install("httpx", keep_going=True) |
| 128 | + import httpx |
| 129 | + await micropip.install('https://raw.githubusercontent.com/psymbio/pyodide_wheels/main/urllib3/urllib3-2.1.0-py3-none-any.whl', keep_going=True) |
| 130 | + import urllib3 |
| 131 | + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) |
| 132 | +
|
| 133 | + from main import sender_message_proxy |
| 134 | + `); |
| 135 | + |
| 136 | + // Prompt the user for the OpenAI API key |
| 137 | + const apiKey = window.prompt("Please enter your OpenAI API key:"); |
| 138 | + |
| 139 | + // Add the HTML content after Pyodide setup. |
| 140 | + document.body.innerHTML += ` |
| 141 | + <h1>Pyodide Chat with AI Assistant</h1> |
| 142 | + <div id="chatbox"></div> |
| 143 | + <input type="text" id="user-input" placeholder="Type your message..."> |
| 144 | + <button id="send-button">Send</button> |
| 145 | + `; |
| 146 | + |
| 147 | + const sendMessageToPython = pyodide.globals.get("sender_message_proxy"); |
| 148 | + |
| 149 | + // Add event listener to send button |
| 150 | + document.getElementById("send-button").addEventListener("click", () => { |
| 151 | + const userInput = document.getElementById("user-input").value; |
| 152 | + document.getElementById("user-input").value = ""; |
| 153 | + const chatbox = document.getElementById("chatbox"); |
| 154 | + chatbox.innerHTML += `<div><strong>You:</strong> ${userInput}</div>`; |
| 155 | + |
| 156 | + sendMessageToPython(apiKey, userInput); |
| 157 | + }); |
| 158 | + |
| 159 | + // Add event listener for the Enter key on the input field |
| 160 | + document.getElementById("user-input").addEventListener("keypress", (event) => { |
| 161 | + if (event.key === "Enter") { |
| 162 | + document.getElementById("send-button").click(); |
| 163 | + } |
| 164 | + }); |
| 165 | + |
| 166 | + await pyodide.runPythonAsync(` |
| 167 | + from main import main as py_main |
| 168 | +
|
| 169 | + await py_main() |
| 170 | + `); |
| 171 | +} |
| 172 | + |
| 173 | +document.addEventListener("DOMContentLoaded", function() { |
| 174 | + setupPyodide(); |
| 175 | +}); |
| 176 | +``` |
| 177 | + |
| 178 | +#### Aspectos Clave del Código JavaScript |
| 179 | + |
| 180 | +- **Inicialización de Pyodide**: El script inicializa el entorno de Pyodide y |
| 181 | + asegura que los paquetes de Python necesarios estén disponibles a través de |
| 182 | +`micropip`. |
| 183 | + |
| 184 | +- **Solicitud de la Clave API**: Para asegurar la interacción con GPT-3.5 |
| 185 | + Turbo, se requiere una clave API. Esto se obtiene a través de un aviso del |
| 186 | +navegador cuando la aplicación se carga por primera vez. |
| 187 | + |
| 188 | +- **Interoperabilidad JavaScript-Python**: Usando `pyodide.registerJsModule`, |
| 189 | + creamos un puente entre JavaScript y Python. Esto permite que Python llame a |
| 190 | +una función de JavaScript (`displayResponse`), que actualiza el cuadro de chat |
| 191 | +con las respuestas de GPT-3.5 Turbo. |
| 192 | + |
| 193 | +- **Carga de la Lógica del Backend**: La lógica del backend se encapsula en |
| 194 | + Python e integra a través de `pyodide.runPythonAsync`. Esto permite que los |
| 195 | +módulos definidos en Python sean transparentes para JavaScript como funciones |
| 196 | +sincrónicas. |
| 197 | + |
| 198 | +### Python: Manejo de Conversaciones con GPT-3.5 Turbo |
| 199 | + |
| 200 | +El corazón de la aplicación involucra una serie de componentes de Python que |
| 201 | +gestionan la comunicación con la API de OpenAI: |
| 202 | + |
| 203 | +#### Backend de Python (`main.py`) |
| 204 | + |
| 205 | +```python |
| 206 | +import asyncio |
| 207 | +import json |
| 208 | +from urllib.parse import quote_plus |
| 209 | + |
| 210 | +import httpx |
| 211 | +import openai |
| 212 | +from pyodide.ffi import create_proxy |
| 213 | +import urllib3 |
| 214 | + |
| 215 | +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) |
| 216 | + |
| 217 | +import js_module |
| 218 | + |
| 219 | + |
| 220 | +class URLLib3Transport(httpx.AsyncBaseTransport): |
| 221 | + def __init__(self) -> None: |
| 222 | + self.pool = urllib3.PoolManager() |
| 223 | + |
| 224 | + async def handle_async_request(self, request: httpx.Request) -> httpx.Response: |
| 225 | + payload = json.loads(request.content.decode("utf-8").replace("'", '"')) |
| 226 | + urllib3_response = self.pool.request( |
| 227 | + request.method, |
| 228 | + str(request.url), |
| 229 | + headers=request.headers, |
| 230 | + json=payload, |
| 231 | + ) |
| 232 | + content = json.loads( |
| 233 | + urllib3_response.data.decode("utf-8") |
| 234 | + ) |
| 235 | + stream = httpx.ByteStream( |
| 236 | + json.dumps(content).encode("utf-8") |
| 237 | + ) |
| 238 | + headers = [(b"content-type", b"application/json")] |
| 239 | + return httpx.Response(200, headers=headers, stream=stream) |
| 240 | + |
| 241 | + |
| 242 | +client: httpx.AsyncClient = httpx.AsyncClient(transport=URLLib3Transport()) |
| 243 | +openai_client: openai.AsyncOpenAI = openai.AsyncOpenAI( |
| 244 | + base_url="https://api.openai.com/v1/", api_key="", http_client=client |
| 245 | +) |
| 246 | +message_queue: asyncio.Queue[tuple[str, str]] = asyncio.Queue() |
| 247 | +loop: asyncio.AbstractEventLoop | None = None |
| 248 | + |
| 249 | + |
| 250 | +async def handle_message(api_key: str, message: str) -> None: |
| 251 | + openai_client.api_key = api_key |
| 252 | + response = await openai_client.chat.completions.create( |
| 253 | + messages=[ |
| 254 | + { |
| 255 | + "role": "user", |
| 256 | + "content": quote_plus(message), |
| 257 | + } |
| 258 | + ], |
| 259 | + model="gpt-3.5-turbo", |
| 260 | + max_tokens=4096, |
| 261 | + temperature=0.2, |
| 262 | + ) |
| 263 | + await js_module.displayResponse(response.choices[0].message.content) |
| 264 | + |
| 265 | + |
| 266 | +async def receiver() -> None: |
| 267 | + while True: |
| 268 | + api_key, message = await message_queue.get() |
| 269 | + await handle_message(api_key, message) |
| 270 | + |
| 271 | + |
| 272 | +def sender(api_key: str, message: str) -> None: |
| 273 | + message_queue.put_nowait((api_key, message)) |
| 274 | + |
| 275 | + |
| 276 | +async def main() -> None: |
| 277 | + global loop |
| 278 | + |
| 279 | + loop = asyncio.get_running_loop() |
| 280 | + loop.create_task(receiver()) |
| 281 | + while True: |
| 282 | + await asyncio.sleep(0.1) |
| 283 | + |
| 284 | + |
| 285 | +sender_message_proxy = create_proxy(sender) |
| 286 | +``` |
| 287 | + |
| 288 | +#### Funcionalidad Principal |
| 289 | + |
| 290 | +- **Capa de Transporte Personalizada**: Una clase `URLLib3Transport` |
| 291 | + personalizada implementa `httpx.AsyncBaseTransport` para manejar solicitudes |
| 292 | +de red sin depender de la API fetch nativa de JavaScript. Esta capa permite una |
| 293 | +gestión flexible de solicitudes HTTP, incluyendo lógica de reintento y |
| 294 | +gestión de sesiones. |
| 295 | + |
| 296 | +- **Bucle de Eventos Asíncrono**: Al utilizar `asyncio`, la aplicación puede |
| 297 | + gestionar tareas asíncronas de manera eficiente, asegurando que la |
| 298 | +aplicación siga siendo receptiva, incluso cuando se trata de interacciones |
| 299 | +lentas de red. |
| 300 | + |
| 301 | +- **Manejo de Mensajes**: La función `handle_message` gestiona la interacción |
| 302 | + con la API de OpenAI. Construye una solicitud usando la entrada del usuario, |
| 303 | +la envía a GPT-3.5 Turbo, y devuelve la respuesta de la IA al frontend a |
| 304 | +través de la función `displayResponse` proporcionada en `js_module`. |
| 305 | + |
| 306 | +- **Conexión con JavaScript**: El `sender_message_proxy` es un puente |
| 307 | + proporcionado por Pyodide que permite que JavaScript encole mensajes para ser |
| 308 | +procesados por el bucle de eventos de Python. |
| 309 | + |
| 310 | +### Razonamiento y Alternativas |
| 311 | + |
| 312 | +- **URLLib3Transport**: La elección de usar un transporte personalizado en |
| 313 | + lugar de clientes HTTP de nivel superior como `requests` se vuelve necesaria |
| 314 | +debido a las limitaciones de Pyodide con las solicitudes de red, como en la |
| 315 | +[solución del problema en |
| 316 | +GitHub](https://github.com/pyodide/pyodide/issues/4292#issuecomment-1848861037). |
| 317 | +Esta solución permite mayor flexibilidad y compatibilidad dentro del entorno |
| 318 | +web en el que opera Pyodide. |
| 319 | + |
| 320 | +- **Diseño de la Interfaz Interactiva**: Aunque la interfaz de usuario permanece |
| 321 | + minimalista en esta prueba de concepto, cumple con los objetivos actuales |
| 322 | +mientras deja espacio para mejorar con interacciones más ricas, estilo o |
| 323 | +soporte multicliente. |
| 324 | + |
| 325 | +### Desafíos y Consideraciones |
| 326 | + |
| 327 | +Construir esta aplicación presenta un conjunto único de desafíos: |
| 328 | +- **Seguridad**: Gestionar una clave API dentro de una aplicación del lado del |
| 329 | + cliente plantea riesgos de seguridad. Los usuarios deben ser cautelosos y, |
| 330 | +potencialmente, emplear proxies del lado del servidor para cualquier |
| 331 | +implementación en vivo. |
| 332 | + |
| 333 | +- **Rendimiento**: Las limitaciones del navegador y el estado alfa de Pyodide |
| 334 | + implican restricciones de rendimiento. Es aconsejable que esto permanezca en |
| 335 | +estado de prueba de concepto pendiente una mayor optimización. |
| 336 | + |
| 337 | +- **Sobrecarga de Instalación**: La necesidad de empaquetar archivos en formato |
| 338 | + wheel de Python hace que el proceso de carga inicial sea pesado. Se podrían |
| 339 | +explorar técnicas para simplificar la entrega de paquetes a los clientes. |
| 340 | + |
| 341 | +## Conclusión |
| 342 | + |
| 343 | +La aplicación descrita aquí demuestra las posibilidades de combinar Pyodide y |
| 344 | +GPT-3 en un entorno basado en la web, ofreciendo una interfaz interactiva para |
| 345 | +experimentar con capacidades de IA. Aunque sigue siendo una prueba de concepto, |
| 346 | +abre la puerta a futuras iteraciones hacia una aplicación robusta y lista para |
| 347 | +producción. |
| 348 | + |
| 349 | +```{note} |
| 350 | +
|
| 351 | +Puedes encontrar el código completo aquí: https://github.com/jpchauvel/pyodide-chat-gpt |
| 352 | +
|
| 353 | +[¡Pruébalo ahora!](https://chauvel.org/blog/pyodide-chat-gpt/) |
| 354 | +
|
| 355 | +``` |
0 commit comments