ساخت و استقرار یک وب API با پشتیبانی از ChatGPT
ChatGPT ابزار جالبی است. انجام برخی کارها را که قبلا سخت بود بسیار آسان می کند. در برخی موارد، مفاهیم جدیدی را معرفی می کند که ممکن است قبلاً فکر نمی کردید انجام دهید.
OpenAI تعدادی sdk را برای کمک به ایجاد در بالای ChatGPT ارائه می دهد.
در این پست، یک ایده ساده را که با استفاده از ChatGPT پیاده سازی شده است، می گیریم و آن را اجرا می کنیم. در یک پست بعدی، نحوه عملکرد برنامه در تولید را بررسی خواهیم کرد. همچنین نحوه غلبه بر برخی از چالش های استفاده از ChatGPT در یک برنامه زنده را بررسی خواهیم کرد.
توجه: برای پیگیری کامل باید برای خدمات مختلف ثبت نام کنید. به طور خاص OpenAI رایگان نیست اگرچه یک آزمایش رایگان دارد.
برنامه نمونه ما
در دوره مهندسی DeeplearningAI Prompt، بخشی در مورد خلاصه کردن محتوا وجود دارد.
این یک عملکرد جالب است و انجام آن بدون استفاده از LLM یا نوعی NLP بسیار دشوار است.
در برنامه نمونه خود، از API NYTimes برای معرفی داستان های برتر امروز استفاده خواهیم کرد. ما ChatGPT را برای خلاصه کردن داستان ها دریافت می کنیم و آن خلاصه را نشان خواهیم داد.
نتیجه نهایی برای مشاهده در دسترس است: https://summer-ui.fly.dev/
کد نمونه برای این در 2 مخزن موجود است:
اگر می خواهید کد UI را نادیده بگیرید زیرا فقط برای نمایش داده های استخراج شده از API ما وجود دارد.
ابزار
API ما با استفاده از FastAPI ساخته شده است که یک چارچوب وب پایتون است.
میتوانیم از API NYTimes برای دریافت برخی اخبار و از OpenAI API برای اجرای ChatGPT استفاده کنیم.
ما می توانیم به fly.io مستقر شویم.
برای رابط کاربری از وانیلی جاوا اسکریپت، CSS و HTML استفاده خواهیم کرد.
GitHub با بخشی در ساخت یک خط لوله استقرار با استفاده از اقدامات GitHub ظاهر خواهد شد. که در قسمت 2 خواهد بود.
ما همچنین در نهایت از Python، Docker و Git استفاده خواهیم کرد.
برپایی
متأسفانه، این راهاندازی کامل نیاز به ثبتنام برای موارد و دریافت کلیدهای API دارد.
امیدوارم تحملم کنی
اگر راهی امن برای ذخیره این کلیدهای API به صورت محلی میخواهید، پست من را در مورد آن بررسی کنید.
یک کلید API NYTimes دریافت کنید
برای این کار، راهنمای تنظیم NYTimes را دنبال کنید.
به اختصار:
-
به اینجا بروید، روی ایجاد حساب کلیک کنید (مگر اینکه قبلاً یک حساب داشته باشید).
- پس از ورود به “برنامه های من” بروید و یک برنامه جدید اضافه کنید.
- وقتی برنامه جدید را انتخاب کنید، باید یک کلید API ببینید.
آن کلید API را کپی کنید و آن را در یک متغیر محیطی به نام در دسترس قرار دهید NYTIMES_API_KEY
.
مطمئن نیستید که منظور من از متغیر مجموعه و محیط چیست؟ این پست را بررسی کنید.
یک کلید ChatGPT API دریافت کنید
هنگامی که کلید خود را دارید، آن را به یک متغیر محیطی به نام اضافه کنید OPENAI_API_KEY
.
توجه: API OpenAI یک سرویس پولی است. با ثبت نام، یک آزمایش رایگان دریافت خواهید کرد. اگر مانند من سال ها پیش ثبت نام کرده اید، آن را فراموش کرده اید و دوره آزمایشی رایگان خود را از دست داده اید، باید یک روش پرداخت اضافه کنید. هر کاری که در حین آزمایش این چیزها انجام دادم حدود 5 سنت هزینه داشت اما 5 یورو به حسابم اضافه کردم. امیدوارم آزمایش رایگان داشته باشید.
Fly.io
اگر می خواهید چیزی را در اینترنت در دسترس قرار دهید، باید آن را در جایی مستقر کنید. تعداد زیادی گزینه وجود دارد. Fly.io یکی از مواردی است که در حال حاضر رایگان است. شما همچنین می توانید یک پایگاه داده رایگان و حتی یک کش ردیس رایگان راه اندازی کنید.
برای یک حساب کاربری رایگان در https://fly.io/ ثبت نام کنید. این تمام چیزی است که در حال حاضر نیاز دارید.
ایجاد اپلیکیشن
این یک راهنمای گام به گام است. می توانید کل این بخش را رد کنید، کد را پایین بکشید و آن را اجرا کنید. اگر می خواهید توضیح عمیق تری در مورد همه بیت ها داشته باشید، ادامه مطلب را بخوانید.
برنامه FastAPI
ما از Python برای API backend استفاده می کنیم. FastAPI یک چارچوب خوب است. هر دو Openai و NYTimes بسته های Python را برای تعامل با API خود ارائه می دهند.
این نیز یک مرحله با گزینه های زیادی است. روش زیر یکی از راههای انجام آن است.
یک دایرکتوری برای پروژه خود ایجاد کنید. من برنامه خود را تابستان نامیدم زیرا به نظر می رسد خلاصه ¯_(ツ)_/¯ مال خود را هر چه می خواهید صدا کنید اما تابستان را در مثال های مختلف جایگزین کنید.
من دوست دارم از دایرکتوری استفاده کنم، تحت فهرست اصلی من، به نام dev
و مخازن من را در آنجا قرار دهم.
توجه: همه این دستورات فرض میکنند که از یک پوسته معقول (نه Cmd.exe) استفاده میکنید.
mkdir ~/dev/summer-api
cd ~/dev/summer-api
من از -api
پسوند را اینجا بگذارید زیرا میخواهم باطن و فرانتنت خود را در دو مخزن مختلف نگه دارم.
git را راه اندازی کنید تا بتوانیم تغییرات خود را در کنترل منبع نگه داریم.
git init
شما باید پایتون را روی سیستم خود نصب کرده باشید. من در حال حاضر از Python 3.11 استفاده می کنم اما اکثر نسخه های 3.8 به بعد باید خوب باشند. اگر قبلاً این کار را نکرده اید، از pyenv برای مدیریت نصب های پایتون خود استفاده کنید.
شعر را نصب کنید:
pip install poetry
ما از شعر استفاده خواهیم کرد زیرا مدیریت وابستگی را ساده میکند و اجرای محیطهای مجازی را آسانتر میکند.
پروژه پایتون خود را با شعر تنظیم کنید:
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"
با خیال راحت نسخه ها را به روز کنید، اگرچه تضمینی برای کار با کد موجود در این پست وجود نخواهد داشت.
حتما اجرا کنید poetry install
اگر به صورت دستی به روز رسانی کنید pyproject.yml
فایل.
یک دایرکتوری برای کد منبع ما ایجاد کنید.
mkdir app
یک فایل اصلی ایجاد کنید.
touch app/__init__.py
touch app/main.py
باز کن app/main.py
در یک ویرایشگر و موارد زیر را وارد کنید:
from fastapi import FastAPI
app = FastAPI()
@app.get("https://dev.to/")
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 ضربه بزنید
curl http://localhost:8000
{"msg":"Welcome to the News App"}
اخبار برتر را از NYTimes دریافت کنید
یک مشتری ایجاد کنید:
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
شما باید در اینجا خروجی بسیار بزرگی با لیستی از stroies و محتوای زیاد داشته باشید.
اخبار را خلاصه کنید
اکنون 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)
قالب بندی داستان ها
در حالی که احتمالاً میتوانستیم ChatGPT را برای کشف JSON از API NYTimes دریافت کنیم، کاهش تعداد مواردی که به آن ارسال میکنیم ضرری ندارد، بنابراین اجازه دهید داستانها را برای ارسال به 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("https://dev.to/")
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 زمان بسیار زیادی طول می کشد. در قسمت 2 این مجموعه به نحوه برخورد با آن مشکل خواهیم پرداخت.
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=["*"],
شما در واقع می خواهید زمانی که URL UI خود را در آن قرار دهید بسیار خاص باشید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);
};
توجه: مطمئن شوید که URL موجود در آن اسکریپت را با هر URL که API شما در آن است جایگزین کنید
اکنون می توانید آن دایرکتوری را به عنوان مثال با استفاده از Live Server در VSCode یا با استفاده از برخی ابزارهای کاربردی دیگر مانند سرویس ارائه دهید.
همه چیز خوب پیش می رود، صفحه ای مانند این را خواهید دید:
استقرار به fly.io
بیایید با باطن شروع کنیم.
ابتدا 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 منحصر به فرد برای برنامه خود خواهید داشت.
برای رابط کاربری ما می توانیم کاری مشابه انجام دهیم.
cd ~/dev/summer-ui
ابتدا اسکریپت را در UI به روز کنید تا به URL جدید برای API شما اشاره کند.
پس این بیت:
window.onload = function () {
fetch("https://summer-api.fly.dev/news/")
در آنجا، شما باید جایگزین کنید "https://summer-api.fly.dev/news/"
با URL شما
سپس یک Dockerfile ایجاد کنید:
touch Dockerfile
محتوای زیر را به Dockerfile اضافه کنید:
FROM pierrezemb/gostatic
COPY ./app/ /srv/http/
EXPOSE 8043
8043
به طور پیش فرض پورتی است که تصویر از آن استفاده می کند.
پرواز بدو:
fly launch
Fly launch فایلی به نام fly.toml در مخزن شما ایجاد می کند.
من به مشکلی برخوردم که در آن باید آن فایل را به موارد زیر در رابط کاربری خود به روز کنم، زیرا به طور پیش فرض پورت اشتباهی داشت:
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"
شما ممکن است به این نیاز داشته باشید یا نداشته باشید. حتما به روز رسانی کنید app
و primary_region
به مقادیر صحیح برای برنامه شما.
بهبودها
این پست در حال حاضر بزرگ است، بنابراین من تمام پیشرفت ها را در پست خود قرار می دهم.
در پست بعدی نگاه خواهیم کرد:
- راه اندازی خط لوله تحویل مداوم
- از redis برای ذخیره پاسخ های ChatGPT و افزایش سرعت سایت ما استفاده کنید
ما همچنین به برخی از روشهای جالب دیگر که ممکن است بخواهید هنگام ساخت یک برنامه producton در بخش پاداش انجام دهید، نگاه خواهیم کرد.
قسمت 2 به زودی.
با تشکر از شما برای خواندن!