构建和部署由ChatGpt提供动力的Web API
#python #api #fastapi #chatgpt

ChatGPT是一个有趣的工具。它使某些事情曾经很难做到很容易。在某些情况下,它引入了您可能从未想过的新概念。

OpenAi提供了一些SKD,以帮助建立在Chatgpt上。

在这篇文章中,我们将采用一个简单的想法,并使用chatgpt进行部署。在后续文章中,我们将研究如何在生产中操作应用程序。我们还将研究如何克服在实时应用程序中使用chatgpt的一些挑战。

我们的示例应用程序

DeeplearningAI Prompt Engineering Course中,有一个关于汇总内容的部分。

这是一个有趣的功能,并且不使用LLM或某种形式的NLP

在我们的示例应用程序中,我们将使用NYTimes API来吸引今天的顶级故事。我们将得到总结的故事,我们将展示这一摘要。

最终结果可在以下位置查看:https://summer-ui.fly.dev/

示例代码可在2个存储库中提供:

如果您愿意,请随时忽略UI代码,因为它只是在那里显示从我们的API中提取的数据。

工具

我们的API是使用FastAPI构建的,这是一个Python Web框架。

我们可以使用NYTimes API获取一些新闻和OpenAI API来运行chatgpt。

我们可以部署到fly.io

对于UI,我们将使用Vanilla JavaScript,CSS和HTML。

GitHub将出现,其中一节使用GitHub actions构建部署管道。那将是第2部分。

我们还将最终使用PythonDocker

设置

不幸的是,这种完整的设置需要和大量注册内容并获取API键。

Image description

我希望你能忍受我。

如果您想在本地存储这些API键的安全方法,请查看my post on that

获取nytimes API密钥

为此,请按照NYTimes setup guide

简而言之:

  • Go here,单击创建帐户(除非您已经有一个)。
  • 登录一旦登录到“我的应用程序”并添加一个新应用。
  • 您选择了新应用程序,您应该看到一个API键。

复制该API键,并在称为NYTIMES_API_KEY的环境中可用。

不确定我所说的设置和环境变量是什么?查看this post

获取chatgpt API密钥

拥有密钥后,将其添加到称为OPENAI_API_KEY的环境变量中。

注意:OpenAI的API是一项付费服务​​。注册时,您将获得免费试用。如果像我一样,您很久以前就注册了,忘记了它并失去了免费试用,则需要添加付款方式。我在测试这些东西时所做的一切都花费了约5美分,但我的帐户增加了5欧元。希望您有免费试用。

fly.io

如果您想在Internet上提供一些可用的东西,则需要在某个地方部署它。有很多选择。 Fly.io是当前免费的。您还可以设置免费的数据库,甚至可以设置免费的Redis Cache。

https://fly.io/上注册免费帐户。这就是您目前需要的。

创建应用程序

这是一个分步指南。您可以跳过整个部分,拉下代码并运行它。如果您想对所有位更深入的解释,请继续阅读。

FastApi应用

我们将使用Python进行后端API。 Fastapi是一个不错的框架。 Openai和Nytimes都提供了用于与API互动的Python软件包。

这再次是有很多选择的一步。以下是这样做的一种方法。

为您的项目创建目录。我打电话给我的应用程序夏天,因为听起来像是总结â_(â_/ââ ^ that your hound hound hound hound hound hound thup summer in the hy.

我喜欢在我的主目录下使用一个名为dev的目录,然后将我的存储库放在那里。

注意:所有这些命令都假定您使用的是合理的外壳(不是cmd.exe)。

mkdir ~/dev/summer-api
cd ~/dev/summer-api

我在这里使用-api后缀,因为我想在两个不同的存储库中保持后端和前端。

初始化git,以便我们可以在源控制中保持更改。

git init

您必须在系统上安装Python。我目前正在使用Python 3.11,但是3.8的大多数版本都可以。如果还没有,请考虑使用pyenv来管理Python安装。

安装poetry

pip install poetry

我们将使用诗歌,因为它简化了依赖性管理,并使运行virtual environments更容易。

用诗歌设置我们的Python项目:

poetry init

您将被要求提供各种输入。他们中的大多数都是显而易见的。最主要的是匹配的依赖。

Package name [summer-api]:
Version [0.1.0]:
Description []:  Summarising the news!
Author [Your name here, n to skip]:  Your name
License []:  MIT
Compatible Python versions [^3.11]:

Would you like to define your main dependencies interactively? (yes/no) [yes]

您可以在初始化期间添加依赖项,也可以跳过该依赖项,然后将下面列出的依赖项粘贴到pyproject.toml文件中,从而导致这样做:

[tool.poetry]
name = "summer-api"
version = "0.1.0"
description = "Summarising the news!"
authors = ["Your name"]
license = "MIT"
readme = "README.md"
packages = [{include = "summer_api"}]

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.95.2"
uvicorn = {extras = ["standard"], version = "^0.22.0"}
pynytimes = "^0.10.0"
openai = "^0.27.7"
fastapi-cache2 = "^0.2.1"
redis = "^4.5.5"

[tool.poetry.group.dev.dependencies]
httpx = "^0.24.1"
pytest = "^7.3.1"
black = "^23.3.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

可以随意更新版本,尽管不能保证在本文中使用代码。

如果您手动更新pyproject.yml文件,请确保运行poetry install

为我们的源代码创建目录。

mkdir app

创建一个主文件。

touch app/__init__.py
touch app/main.py

在编辑器中打开app/main.py并输入以下内容:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def index():
    return {"msg": "Welcome to the News App"}

运行应用程序:

poetry run uvicorn app.main:app --reload

您应该看到这样的输出:

INFO:     Will watch for changes in these directories: ['~/dev/summer-api']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [29115] using WatchFiles
INFO:     Started server process [29144]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

您可以在浏览器中访问http://localhost:8000/或用卷发击中,例如

curl http://localhost:8000
{"msg":"Welcome to the News App"}

从纽约时报中获取顶级故事

创建客户端:

touch app/nytimes_client.py

在该文件中,设置一个电话,以获取当今的热门故事:

import os

from pynytimes import NYTAPI
# This is the API Key we setup and added to the env earlier
API_KEY = os.getenv("NYTIMES_API_KEY", "")
nyt = NYTAPI(API_KEY, parse_dates=True)


def get_top_stories():
    return nyt.top_stories()

要测试它,让我们创建和端点,简单地返回顶级故事。

在我们的app/main.py文件中导入我们的新模块:

from .nytimes_client import get_top_stories

添加一个新的端点:

@app.get("/news")
def news():
    return get_top_stories()

运行该应用程序并测试它(如果应用程序已经运行,则--reload标志将其重新启动,因此您不必再次运行它)。

curl http://localhost:8000/news

如果已安装了jq,则可以使用它使输出看起来更好。

curl http://localhost:8000/news | jq

您应该在此处获得一个很大的输出,并有一系列散文和大量内容。

总结新闻

现在我们将介绍chatgpt。

创建一个新文件:

touch app/summariser.py

使用内联注释来解释此文件中的代码。

import os

import openai

# You will need to get your API key from https://platform.openai.com/account/api-keys
openai.api_key = os.getenv("OPENAI_API_KEY")


# Got this function from this amazing course https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers/
def get_completion(prompt, model="gpt-3.5-turbo"):
    messages = [{"role": "user", "content": prompt}]
    response = openai.ChatCompletion.create(
        model=model,
        messages=messages,
        temperature=0,  # this is the degree of randomness of the model's output
    )
    return response.choices[0].message["content"]


def summarise_news_stories(stories):
    """
    Takes in a list of news stories and prompts ChatGPT to
    generate a short summary of each story based on the provided
    Section, Subsection, Title, and Abstract.
    The function returns the summary generated by ChatGPT.

    Args:
    - stories (str): A list of news stories to be summarised.
    Each story should be a block of text with the following format
    where each part is on a newline:

    Section: the section
    Subsection: the subsection
    Title: the title
    Abstract: the Abstract

    The values for 'Subsection' and 'Abstract' can be empty
    strings if not applicable.

    Returns:
    - summary (str): A string containing the summary generated by ChatGPT
    for all the news stories.
    """

    print("Beginning summary")
    prompt = f"""
    Your task is to generate a short summary of a series of
    news stories given the Section, Subsection, Title and
    Abstract, of each story.

    The sections are after the 'Section:'.
    The subsections are after the 'Subsection:'. The subsections can be empty.

    The title is after the 'Title:'. The abstract is after the 'Abstract:'.
    The abstract can be empty.

    Summarise the stories below, delimited by triple backticks,
    in the style of an AI trying to convey information to humans.
    Please use paragraphs where appropriate and use at most 800 words.

    Stories: ```{stories}```
    """

    return get_completion(prompt)

格式化故事

虽然我们可能会从nytimes API中弄清楚JSON,但减少我们发送给它的东西的数量是没有害处的,所以让我们格式化故事发送给chatgpt。

创建另一个文件:

touch app/story_formatter.py

在该文件中添加一个函数以格式化故事,因此我们只发送我们需要的内容:

def format_stories_to_string(stories):
    stories_string = ""
    for story in stories:
        title = story["title"]
        abstract = story["abstract"]
        section = story["section"]
        subsection = story["subsection"]
        stories_string += f"""
        Section: {section}
        Subsection: {subsection}
        Title: {title}
        Abstract: {abstract}
        """
    return stories_string

新闻终点

现在我们可以将所有内容整合在一起。更新我们的app/main.py文件以显示以下内容:

import os

from fastapi import FastAPI, HTTPException

from .nytimes_client import get_top_stories
from .story_formatter import format_stories_to_string
from .summariser import summarise_news_stories

app = FastAPI()


@app.get("/")
def index():
    return {"msg": "Welcome to the News App"}


@app.get("/news")
def news():
    summary = ""
    images = []
    try:
        stories = get_top_stories()
        for story in stories:
            images.extend(story["multimedia"])
        summary = summarise_news_stories(format_stories_to_string(stories))
        images = list(
            filter(lambda image: image["format"] == "Large Thumbnail", images)
        )
    except Exception as e:
        print(e)
        raise HTTPException(
            status_code=500, detail="Apologies, something bad happened :("
        )
    return {"summary": summary, "images": images}

您现在可以对其进行测试,但是请注意,致电Chatgpt需要很长时间。我们将研究如何在下一节中处理该问题。

curl http://localhost:8000/news

这应该输出新闻的摘要以及所有相关图像的列表。

在为此设置UI之前,我们需要另一个步骤。我们的API希望浏览器中的任何呼叫都来自与应用程序相同的URL。这是一种称为Cross-Origin Resource Sharing (CORS)的安全机制。我们需要更新API,以允许我们的UI请求。

在我们的main.py中添加和配置CORS中间件:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

注意:对于allow_origins=["*"],,您实际上想非常具体,一旦您知道UI的URL为例如。 allow_origins=["http://localhost:8080"],。在此处使用通配符,因此它可以正常工作,但请记住这一点。在GitHub上的示例代码中,有一个更好的示例。

设置我们的UI

这是完全可选的,因此我们将迅速通过步骤。

mkdir ~/dev/summer-ui
cd  ~/dev/summer-ui
git init
mkdir app
touch app/index.html
touch app/script.js
touch app/style.css

app/index.html文件中:

<!DOCTYPE html>
<html>
<head>
    <title>Today's News</title>
    <link rel="stylesheet" type="text/css" href="styles.css">
    <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests" />
</head>
<body>
    <main>
        <div id="loader">
            <h1>This should be quick, but ever so often it takes a long time...</h1>
            <div class="lds-circle"><div></div></div>
        </div>
        <div id="news-container" class="container hidden">
            <h1>Today's News</h1>
            <div class="date" id="date"></div>


            <p id="summary">
                <!-- Summary will be inserted here -->
            </p>
        </div>
        <div id="images" class="img-container">
            <!-- Images will be inserted here -->
        </div>
    </main>
    <script src="script.js"></script>
</body>
</html>

app/style.css文件中:

body {
  margin: 0;
  padding: 0;
  font-family: Arial, sans-serif;
  color: #333;
  line-height: 1.6;
  background: rgb(238, 174, 202);
  background: radial-gradient(
    circle,
    rgba(238, 174, 202, 1) 0%,
    rgba(148, 187, 233, 1) 100%
  );
}

.container {
  width: 80%;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  background-color: #fff;
  box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.1);
  padding-bottom: 4rem;
}

h1 {
  font-size: 2em;
  margin-bottom: 0.5em;
  color: #212121;
  text-align: center;
}

p {
  font-size: 1em;
  text-align: justify;
}

/* Media queries for responsive design */
@media (max-width: 768px) {
  /* Tablets */
  .container {
    width: 90%;
  }

  h1 {
    font-size: 1.75em;
  }

  p {
    font-size: 0.9em;
  }
}

@media (max-width: 480px) {
  /* Phones */
  .container {
    width: 95%;
  }

  h1 {
    font-size: 1.5em;
  }

  p {
    font-size: 0.85em;
  }
}

.img-container {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
}

.photo {
  position: relative;
  margin: -15px; /* negative margin makes the images overlap */
  transform: rotate(-10deg); /* starting angle */
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
  transition: transform 0.5s;
  z-index: 1; /* ensure that images can stack on top of each other */
}

.photo:hover {
  transform: rotate(0deg); /* reset to straight when hovered */
}

.photo img {
  max-width: 150px;
  border-radius: 10px;
}

.date {
  text-transform: uppercase;
  text-align: center;
}

#loader {
  padding-top: 20%;
  text-align: center;
}

.lds-circle {
  display: inline-block;
  transform: translateZ(1px);
}
.lds-circle > div {
  display: inline-block;
  width: 64px;
  height: 64px;
  margin: 8px;
  border-radius: 50%;
  background: #fff;
  animation: lds-circle 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
@keyframes lds-circle {
  0%,
  100% {
    animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5);
  }
  0% {
    transform: rotateY(0deg);
  }
  50% {
    transform: rotateY(1800deg);
    animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1);
  }
  100% {
    transform: rotateY(3600deg);
  }
}

.hidden {
  display: none;
}

app/script.js文件中:

window.onload = function () {
  fetch("https://summer-api.fly.dev/news/")
    .then((response) => response.json())
    .then((data) => {
      document.getElementById("loader").style.display = "none";
      document.getElementById("news-container").style.display = "block";

      const summaryElement = document.getElementById("summary");
      const imagesElement = document.getElementById("images");

      summaryElement.innerHTML = data.summary.split("\n").join("<br>");

      data.images.forEach((image, index) => {
        const img = document.createElement("img");
        img.className = "photo";
        img.src = image.url;
        img.alt = image.caption;
        img.style.width = "150px";
        img.style.height = "150px";

        // Every third image will take up double the width and height
        if (index % 3 === 0) {
          img.style.gridColumnEnd = "span 2";
          img.style.gridRowEnd = "span 2";
        }

        imagesElement.appendChild(img);
      });
    })
    .catch((err) => console.error(err.message));

  function formatDate(date) {
    const options = {
      weekday: "long",
      month: "long",
      day: "numeric",
      year: "numeric",
    };
    return date.toLocaleDateString("en-US", options);
  }

  const today = new Date();
  document.getElementById("date").textContent = formatDate(today);
};

注意:确保用

的API替换该脚本中的URL

现在您可以使用该目录,例如使用Live Server in VSCode或使用其他一些实用程序,例如serve

一切顺利,您会看到这样的页面:

Image description

部署到飞行

让我们从后端开始。

首先,安装flyctl https://fly.io/docs/hands-on/install-flyctl/

登录:

fly auth login

转到我们的API目录:

cd ~/dev/summer-api

部署服务的最简单方法是使用dockerfile来定义环境。如果您可以在本地构建并运行它,则在部署时可能会起作用。

创建一个dockerfile。

touch Dockerfile

使用此内容:

FROM python:3.11 as requirements-stage

WORKDIR /tmp

RUN pip install poetry

COPY ./pyproject.toml ./poetry.lock* /tmp/

RUN poetry export -f requirements.txt --output requirements.txt --without-hashes

FROM python:3.11

WORKDIR /code

COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY ./app /code/app

EXPOSE 8080

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

测试它:

docker build . -t summer-api
docker run -p 8080:8080 summer-api

您应该能够在http://localhost:8080点击您的申请。

fly知道如何部署Dockerfile。

fly launch

将指导您设置应用程序。

我们还需要给它我们的API键。

flyctl secrets set NYTIMES_API_KEY=${NYTIMES_API_KEY}
flyctl secrets set OPENAI_API_KEY=${OPENAI_API_KEY}

一切顺利,您的API应该在运行。例如,我的位于这里:https://summer-api.fly.dev/

您将为您的应用具有唯一的URL。

对于UI,我们可以做类似的事情。

cd ~/dev/summer-ui

首先,在UI中更新脚本,以便指向API的新URL。

所以这个位:

window.onload = function () {
  fetch("https://summer-api.fly.dev/news/")

在那里,您应该用URL替换"https://summer-api.fly.dev/news/"

然后创建一个dockerfile:

touch Dockerfile

将以下内容添加到dockerfile:

FROM pierrezemb/gostatic
COPY ./app/ /srv/http/

EXPOSE 8043

8043恰好是图像默认使用的端口。

奔跑飞:

fly launch

fly启动在您的存储库中创建一个名为fly.toml的文件。

我遇到了一个问题,我需要在UI中将该文件更新为以下内容,因为它默认为错误的端口:

app = "summer-ui"
primary_region = "cdg"


kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 8043
  processes = ["app"]
  protocol = "tcp"
  script_checks = []

  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  [[services.ports]]
    handlers = ["http"]
    port = 80
    force_https = true

  [[services.ports]]
    handlers = ["tls", "http"]
    port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

您可能需要或不需要。确保将appprimary_region更新为应用程序的正确值。

改进

这篇文章已经很大,因此我将所有的改进都投入到他们自己的帖子中。

在下一篇文章中,我们将查看:

  • 设置连续交货管道
  • 使用Redis兑现ChatGpt响应并加快我们的网站

我们还会研究您在奖金部分构建Producton应用程序时可能想做的其他一些有趣的实践。

第2部分即将推出。

感谢您的阅读!