Skip to content

Commit 4f24361

Browse files
authored
Adding Pyodide Chat GPT Blogpost (#82)
* adding pyodide chat gpt blogpost * lapsus brutus * rewording things
1 parent 52ab1ff commit 4f24361

File tree

1 file changed

+355
-0
lines changed

1 file changed

+355
-0
lines changed
Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
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+
![OpenAI](/_static/images/openai.png){ 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

Comments
 (0)