DevGang
Авторизоваться

Создание потокового ИИ-ассистента с помощью ChatGPT, FastAPI, WebSockets и React

Генеративный предварительно обученный трансформатор (Generative Pre-Trained Transformer, GPT) - это разновидность большой языковой модели (Large Language Model, LLM), которая в этом году стала горячей темой в мире технологий, и многие компании спешат добавить эту технологию в свои продукты. Создание и обучение этих больших моделей может быть очень сложным, трудоемким и дорогостоящим процессом. Вы можете подумать, что вы не сможете использовать эту технологию, поскольку она настолько сложна и дорога, но такие компании, как OpenAI, проделали огромную работу по созданию полезных моделей и создали платформы, предоставляющие API для их использования. Если вы когда-нибудь использовали API, в котором вы отправляете некоторые данные, он выполняет некую магию за кулисами, и вы получаете некоторые данные, которые можно использовать в ответе, то вы можете интегрировать эту передовую технологию в свое приложение. Давайте рассмотрим, как можно создать веб-приложение полного стека, позволяющее задавать вопросы OpenAI и получать ответ в потоковом режиме.

⭐️ Полный исходный код, на который ссылается данное руководство, доступен на GitHub.

https://github.com/dpills/ai-assistant

Для использования OpenAI API необходимо зарегистрироваться, затем сгенерировать API-ключ и добавить его в файл .env в папке нового проекта.

📝 .env
OPENAI_API_KEY=sk-YWUedpcl1xiGvGiD4xTwT3TlbkFJx9Sgnt8s0QYNxxxxxxxx

Установите зависимости API вместе с библиотекой openai python.

📝 pyproject.toml
[tool.poetry]
name = "ai-assistant"
version = "0.1.0"
description = ""
authors = ["dpills"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.11"
openai = "^1.2.3"
python-dotenv = "^1.0.0"
fastapi = "^0.104.1"
uvicorn = { extras = ["standard"], version = "^0.24.0.post1" }
websockets = "^12.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
$ poetry install
...
  • Installing fastapi (0.104.1)
  • Installing openai (1.2.3)
...

Создайте файл python и импортируйте библиотеку OpenAI, которая будет использовать для аутентификации ключ OPENAI_API_KEY из переменных окружения. В опциях установите значение stream в true и используйте асинхронный генератор для передачи фрагментов ответа по мере их возврата. Мы используем модель GPT-3.5 Turbo, которая доступна в бесплатной версии, но вы можете заменить ее на более новую модель, например GPT-4, если у вас есть к ней доступ.

📝 main.py
from typing import AsyncGenerator

from dotenv import load_dotenv
from openai import AsyncOpenAI

load_dotenv()

client = AsyncOpenAI()

async def get_ai_response(message: str) -> AsyncGenerator[str, None]:
    """
    OpenAI Response
    """
    response = await client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[
            {
                "role": "system",
                "content": (
                    "You are a helpful assistant, skilled in explaining "
                    "complex concepts in simple terms."
                ),
            },
            {
                "role": "user",
                "content": message,
            },
        ],
        stream=True,
    )

    all_content = ""
    async for chunk in response:
        content = chunk.choices[0].delta.content
        if content:
            all_content += content
            yield all_content

Это базовая настройка для добавления ChatGPT в ваше приложение, которая довольно проста!

Теперь добавим конечную точку WebSocket API с поддержкой FastAPI WebSocket, чтобы иметь постоянное двунаправленное соединение, по которому мы можем передавать ответ от ChatGPT в наше веб-приложение в режиме реального времени.

📝 main.py
from typing import AsyncGenerator, NoReturn

import uvicorn
from dotenv import load_dotenv
from fastapi import FastAPI, WebSocket
from openai import AsyncOpenAI

load_dotenv()

app = FastAPI()
client = AsyncOpenAI()

...

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> NoReturn:
    """
    Websocket for AI responses
    """
    await websocket.accept()
    while True:
        message = await websocket.receive_text()
        async for text in get_ai_response(message):
            await websocket.send_text(text)

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8000,
        log_level="debug",
        reload=True,
    )

Наконец, для этого простого примера мы можем просто заставить API обслуживать наше Web-приложение с файлом index.html на корневом маршруте.

📝 main.py
from typing import AsyncGenerator, NoReturn

import uvicorn
from dotenv import load_dotenv
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
from openai import AsyncOpenAI

load_dotenv()

app = FastAPI()
client = AsyncOpenAI()

with open("index.html") as f:
    html = f.read()

async def get_ai_response(message: str) -> AsyncGenerator[str, None]:
    """
    OpenAI Response
    """
    response = await client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=[
            {
                "role": "system",
                "content": (
                    "You are a helpful assistant, skilled in explaining "
                    "complex concepts in simple terms."
                ),
            },
            {
                "role": "user",
                "content": message,
            },
        ],
        stream=True,
    )

    all_content = ""
    async for chunk in response:
        content = chunk.choices[0].delta.content
        if content:
            all_content += content
            yield all_content

@app.get("/")
async def web_app() -> HTMLResponse:
    """
    Web App
    """
    return HTMLResponse(html)

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket) -> NoReturn:
    """
    Websocket for AI responses
    """
    await websocket.accept()
    while True:
        message = await websocket.receive_text()
        async for text in get_ai_response(message):
            await websocket.send_text(text)

if __name__ == "__main__":
    uvicorn.run(
        "main:app",
        host="0.0.0.0",
        port=8000,
        log_level="debug",
        reload=True,
    )

Теперь создайте файл index.html, используя версию React для CDN-разработок, транспилированную с автономной версией Babel и библиотекой React Material UI. Приложение подключается к веб-сокету и отправляет новые вопросы в WebSocket API, который затем передает ответ по мере его генерации и отображает разметку.

📝 index.html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <title>AI Assistant 🤓</title>
    <meta name="viewport" content="initial-scale=1, width=device-width" />
    <script src="https://unpkg.com/react@latest/umd/react.development.js" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/react-dom@latest/umd/react-dom.development.js"></script>
    <script src="https://unpkg.com/@mui/material@latest/umd/material-ui.development.js"
        crossorigin="anonymous"></script>
    <script src="https://unpkg.com/@babel/standalone@latest/babel.min.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link rel="stylesheet"
        href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap" />
    <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
</head>

<body>
    <div id="root"></div>
    <script type="text/babel">
        const {
            colors,
            CssBaseline,
            ThemeProvider,
            Typography,
            TextField,
            Container,
            createTheme,
            Box,
            Skeleton,
        } = MaterialUI;

        const theme = createTheme({
            palette: {
                mode: 'dark'
            },
        });
        const WS = new WebSocket("ws://localhost:8000/ws");

        function App() {
            const [response, setResponse] = React.useState("");
            const [question, setQuestion] = React.useState("");
            const [loading, setLoading] = React.useState(false);

            React.useEffect(() => {
                WS.onmessage = (event) => {
                    setLoading(false);
                    setResponse(marked.parse(event.data));
                };
            }, []);

            return (
                <Container maxWidth="lg">
                    <Box sx={{ my: 4 }}>
                        <Typography variant="h4" component="h1" gutterBottom>
                            AI Assistant 🤓
                        </Typography>
                        <TextField
                            id="outlined-basic"
                            label="Ask me Anything"
                            variant="outlined"
                            style={{ width: '100%' }}
                            value={question}
                            disabled={loading}
                            onChange={e => {
                                setQuestion(e.target.value)
                            }}
                            onKeyUp={e => {
                                setLoading(false)
                                if (e.key === "Enter") {
                                    setResponse('')
                                    setLoading(true)
                                    WS.send(question);
                                }
                            }}
                        />
                    </Box>
                    {!response && loading && (<>
                        <Skeleton />
                        <Skeleton animation="wave" />
                        <Skeleton animation={false} /></>)}
                    {response && <Typography dangerouslySetInnerHTML={{ __html: response }} />}
                </Container>
            );
        }

        ReactDOM.createRoot(document.getElementById('root')).render(
            <ThemeProvider theme={theme}>
                <CssBaseline />
                <App />
            </ThemeProvider>,
        );
    </script>
</body>

</html>

Теперь мы готовы к тестированию! Запустите python-скрипт, который запустит API и обслужит Web-приложение.

$ python3 main.py
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
DEBUG:    = connection is CONNECTING
DEBUG:    < GET /ws HTTP/1.1
DEBUG:    < user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
DEBUG:    < upgrade: websocket
DEB**UG:    < origin: http://localhost:8000**
DEBUG:    < sec-websocket-version: 13
DEBUG:    < cookie: ajs_anonymous_id=57154c91-3ec5-4308-beaa-25a353f3ce66
DEBUG:    < sec-websocket-key: BQOnRY2biub2alMCN7ax+w==
DEBUG:    < sec-websocket-extensions: permessage-deflate; client_max_window_bits
INFO:     ('127.0.0.1', 54308) - "WebSocket /ws" [accepted]
DEBUG:    > date: Sun, 12 Nov 2023 19:00:20 GMT
DEBUG:    > server: uvicorn
INFO:     connection open
DEBUG:    = connection is OPEN

Перейдите по адресу http://localhost:8000, введите вопрос и нажмите клавишу Enter.

Наш вопрос отправляется в API через WebSocket, затем в OpenAI, который генерирует ответ, который передается обратно через WebSocket и отображается в веб-приложении!

Видно, что на генерацию полного ответа ChatGPT уходит около 30 секунд, что для любого приложения очень долго. Но поскольку мы используем потоковую передачу ответа через WebSocket, это делает его очень отзывчивым. Данные ответа выводятся на экран быстрее, чем обычный человек успевает их прочитать, что повышает удобство работы.

Надеюсь, это показало вам, как легко можно добавить улучшения искусственного интеллекта в любое приложение, и дало вам несколько базовых элементов для создания отличного опыта для ваших пользователей!

Источник:

#React #Python #FastAPI
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу