Chapter 4. ํ๋ฌ๊ทธ์ธ ์ ์ํด๋ณด๊ธฐ (feat. ChatGPT)
๋์น๊ฐ ๋น ๋ฅธ ๋ ์๋ค์ "์ด์ฐจํผ ์ด๊ฑธ ์ ๋ ๊ฒ์ ๋น์ ์ด ์๋ ๊ฒ์ด๋ค"์์ ์ด ๊ธ์ ์ ๋ชฉ์ ์๋ฏธ๋ฅผ ์์๋ดค์ ์๋ ์๋ค
๊ทธ๋ ๋ค. '์ฐ๋ฆฌ'๋ ํ๋ฌ๊ทธ์ธ์ ์ฝ๋๋ฅผ ์์ฑํ์ง ์์ ๊ฒ์ด๋ค.
์๋? ํ๋ฌ๊ทธ์ธ ์ฝ๋๋ ์ฐ๋ฆฌ ๋๋ถ๋ถ๋ณด๋ค ChatGPT๊ฐ ๋ ์ ์ฐ๋.
์์ผ๋ก ์ฐ๋ฆฌ๋, '๋ธ๊น' ํ ๋ฒ์ผ๋ก ํ๋ฌ๊ทธ์ธ์ ์ ์ํ ๊ฑฐ๋ค. ์ค๋ ์ง ์๋๊ฐ?
์ด ๊ธ์์ ํ๋กฌํํธ๋ฅผ ์์ฑํด ์คํ ๋, ๋ณธ์ธ์ ๊ธฐํธ์ ๋ง๊ฒ ๊ดํธ์์ ๋ด์ฉ์ ์์ ๋ง ํ๋ฉด ๋๋ค!
GPT 3.5 ๋ณด๋จ 4๋ฅผ, ํ๊ตญ์ด๋ณด๋จ ์์ด๋ฅผ ์ฐ๋ ๊ฒ์ ์ถ์ฒํ๋ค
*ํ๋กฌํํธ๋ ํ๋์ ๋ฐํ์ผ๋ก ํ์ํจ
1. ํ๋ฌ๊ทธ์ธ ์์ด๋์ด ์ ๋ฆฌํ๊ธฐ
๋ ์ง๊ธ๋ถํฐ (๋ณธ์ธ์ด ์ํ๋ ํ๋ฌ๊ทธ์ธ ๊ธฐ๋ฅ) ์ ํ๋ ChatGPT ํ๋ฌ๊ทธ์ธ์ ๋ง๋๋ ๊ฐ๋ฐ์์ผ
์๋ ๋น์นธ($ ํ์)์ ์์ด๋ก ์ฑ์๋ด
Scope and idea for the app:
$GENERAL_PROJECT_SCOPE
The plugin has one main functionality which allows the user to say something like:
$EXAMPLE_PROMPTS
The endpoint will receive:
$INFO_NEEDED_FOR_FUNCTIONALITY (eg, date, stock name, url)
ChatGPT will take in the userโs prompt and extract these fields before sending it to the API endpoint.
The user is not able to:
$FUNCTIONALITY_LIMIT
์ ํ๋กฌํํธ๋ ChatGPT์๊ฒ ๋น์ ์ด ๋ง๋ค๊ณ ์ ํ๋ ํ๋ฌ๊ทธ์ธ์ ์ค๋ช ํด์ฃผ๊ณ
๊ทธ ๋ด์ฉ์ ChatGPT๊ฐ ์์์ ๋ฐ์ ์์ผ (+์์ด๋ก) ์ ๋ฆฌํ๋๋ก ์๋ํ ํด์ฃผ๋ ํ๋กฌํํธ๋ค
์ด ํ๋กฌํํธ์ ๋ํ ๋ต์ฅ์ ๋ณต์ฌํ๊ณ 2๋ฒ์ผ๋ก ๋์ด๊ฐ์
2. ํ๋ฌ๊ทธ์ธ ์ฝ๋ ์์์์ฑํ๊ธฐ
(1๋ฒ ํ๋กฌํํธ๋ก ์ป์ ๋ต์ฅ)
Information on any API's that will be used to create this app and the docs for the functionality required:
(์ฌ์ฉํ๋ ค๋ API์ ๋ํ ์๋ฃ)
Below is a main.py file showing an example of a basic plugin app:
import json
import quart
import quart_cors
from quart import request
# Note: Setting CORS to allow chat.openapi.com is only required when running a localhost plugin
app = quart_cors.cors(quart.Quart(__name__), allow_origin="https://chat.openai.com")
_TODOS = {}
@app.post("/todos/<string:username>")
async def add_todo(username):
request = await quart.request.get_json(force=True)
if username not in _TODOS:
_TODOS[username] = []
_TODOS[username].append(request["todo"])
return quart.Response(response='OK', status=200)
@app.get("/todos/<string:username>")
async def get_todos(username):
return quart.Response(response=json.dumps(_TODOS.get(username, [])), status=200)
@app.delete("/todos/<string:username>")
async def delete_todo(username):
request = await quart.request.get_json(force=True)
todo_idx = request["todo_idx"]
if 0 <= todo_idx < len(_TODOS[username]):
_TODOS[username].pop(todo_idx)
return quart.Response(response='OK', status=200)
@app.get("/logo.png")
async def plugin_logo():
filename = 'logo.png'
return await quart.send_file(filename, mimetype='image/png')
@app.get("/.well-known/ai-plugin.json")
async def plugin_manifest():
host = request.headers['Host']
with open("ai-plugin.json") as f:
text = f.read()
# This is a trick we do to populate the PLUGIN_HOSTNAME constant in the manifest
text = text.replace("PLUGIN_HOSTNAME", f"https://{host}")
return quart.Response(text, mimetype="text/json")
@app.get("/openapi.yaml")
async def openapi_spec():
host = request.headers['Host']
with open("openapi.yaml") as f:
text = f.read()
# This is a trick we do to populate the PLUGIN_HOSTNAME constant in the OpenAPI spec
text = text.replace("PLUGIN_HOSTNAME", f"https://{host}")
return quart.Response(text, mimetype="text/yaml")
def main():
app.run(debug=True, host="0.0.0.0", port=5002)
if __name__ == "__main__":
main()
Important notes:
Be sure to set a conservative limit on the size of the response the app tries to return to ensure the character limit of 100k characters is not exceeded.
Make sure the API endpoints name eg "/news" matches up with what is written in the OpenAPI definition.
Include a custom logging handler on all visits to URLs and important app events as in the above examples
Do not leave placeholders in, replace all of them with the correct info so that I can copy and paste this and it is ready to launch.
Make sure the routes for '/.well-known/ai-plugin.jsonโ and '/.well-known/openapi.yaml' are reused exactly as shown above.
Now, I need you to create the main.py file for my app.
API์ ๋ํ ์๋ฃ๋ ์ธํฐ๋ท์์ ๊ฒ์ํ ๊ณต์ ๋ฌธ์ ๊ทธ๋ฅ ๋ณต๋ถํ๋ฉด ๋๋ค
ํ์๋ ๋ค์์ ์ด์ฉํ๋ค(https://github.com/jdepoix/youtube-transcript-api)
์ฌ๊ธฐ๊น์ง ํ๋ค๋ฉด ์ถํํ๋ค.
๋ฐฉ๊ธ ํ๋ฌ๊ทธ์ธ์ ํต์ฌ ์ฝ๋๋ฅผ ๋ค ์์ฑํ๋ค.
3. ai-plugin.json ์์ฑํ๊ธฐ
Now I need you to write me a manifest file for my plugin. Here are the OpenAI docs on how to create a manifest file:
Plugin manifest
Every plugin requires a ai-plugin.json file, which needs to be hosted on the APIโs domain. For example, a company called example.com would make the plugin JSON file accessible via an https://example.com domain since that is where their API is hosted. When you install the plugin via the ChatGPT UI, on the backend we look for a file located at /.well-known/ai-plugin.json. The /.well-known folder is required and must exist on your domain in order for ChatGPT to connect with your plugin. If there is no file found, the plugin cannot be installed. For local development, you can use HTTP but if you are pointing to a remote server, HTTPS is required.
The minimal definition of the required ai-plugin.json file will look like the following:
{
"schema_version": "v1",
"name_for_human": "TODO Plugin",
"name_for_model": "todo",
"description_for_human": "Plugin for managing a TODO list. You can add, remove and view your TODOs.",
"description_for_model": "Plugin for managing a TODO list. You can add, remove and view your TODOs.",
"auth": {
"type": "none"
},
"api": {
"type": "openapi",
"url": "<http://localhost:3333/openapi.yaml>",
"is_user_authenticated": false
},
"logo_url": "<http://localhost:3333/logo.png>",
"contact_email": "support@example.com",
"legal_info_url": "<http://www.example.com/legal>"
}
If you want to see all of the possible options for the plugin file, you can refer to the definition below.
FIELD
TYPE
DESCRIPTION / OPTIONS
REQUIRED
schema_version
String
Manifest schema version
โ
name_for_model
String
Name the model will used to target the plugin
โ
name_for_human
String
Human-readable name, such as the full company name
โ
description_for_model
String
Description better tailored to the model, such as token context length considerations or keyword usage for improved plugin prompting.
โ
description_for_human
String
Human-readable description of the plugin
โ
auth
ManifestAuth
Authentication schema
โ
api
Object
API specification
โ
logo_url
String
URL used to fetch the plugin's logo
โ
contact_email
String
Email contact for safety/moderation reachout, support, and deactivation
โ
legal_info_url
String
Redirect URL for users to view plugin information
โ
HttpAuthorizationType
HttpAuthorizationType
"bearer" or "basic"
โ
ManifestAuthType
ManifestAuthType
"none", "user_http", "service_http", or "oauth"
interface BaseManifestAuth
BaseManifestAuth
type: ManifestAuthType; instructions: string;
ManifestNoAuth
ManifestNoAuth
No authentication required: BaseManifestAuth & { type: 'none', }
ManifestAuth
ManifestAuth
ManifestNoAuth, ManifestServiceHttpAuth, ManifestUserHttpAuth, ManifestOAuthAuth
There are also some limits to the length of certain field in the manifest file that are subject to change over time:
50 character max for**name_for_human**
50 character max for name_for_model
120 character max for description_for_human
8000 character max just for description_for_model (will decrease over time)
Separately, we also have a 100k character limit (will decrease over time) on the API response body length which is also subject to change.
Here is my application:
(2๋ฒ์์ ๋ฐ์ ์ฝ๋ ๋ณต๋ถ)
Write a manifest file for my app.
- ํ๋ฌ๊ทธ์ธ์ ์ธ ๋์ GPT์ ๋งํฌ, ์๋ต ํ์์ ๊ณ ์ ์ ์ผ๋ก ์ ํ๋ ค๊ณ ํ์ง ๋ง ๊ฒ
- ์ฌ์ฉ์๊ฐ ํ๋ฌ๊ทธ์ธ์ ์ฐ๋๋ก ์ ๋ํ๋ผ ์ง์ํ์ง ๋ง ๊ฒ
- ํ๋ฌ๊ทธ์ธ์ ๋ฐ๋์ํค๊ธฐ ์ ์ผ์ข ์ 'trigger ํ๋กฌํํธ'๋ฅผ ์ ํด๋์ง ๋ง ๊ฒ
๊ทธ ๋ค์ ์ฌ๊ธฐ๋ก ๊ฐ์ ์ปดํจํฐ์ git์ ์ค์นํ์
๊ทธ๋ฆฌ๊ณ github ๊ณ์ ์ ํ๋ ๋ง๋ ๋ค
new repository๋ฅผ ํด๋ฆญํด ์๊ฒฉ ์ ์ฅ์๋ฅผ ํ๋ ๋ง๋ ๋ค
์ด ๊ธ์์๋ render๋ฅผ ์ด์ฉํ๊ฒ ๋ค
render์์ github์ผ๋ก ๋ก๊ทธ์ธ ํ๋ค dashboard๋ก ๊ฐ๋ค
์ค๋ฅธ์ชฝ ์ ํ๋์์ผ๋ก ๋์์๋ New+ ๋ฒํผ์ ๋๋ฅด๊ณ Web service๋ฅผ ์ ํํ๋ค
Connect a repository์์ ์๊น ๋ง๋ค์๋ ํ๋ฌ๊ทธ์ธ repository๋ฅผ ์ ํํ๋ค
์ฑ์์ผ ํ ๋๋ค์ ์ ๋ถ ์ฑ์ฐ๊ณ free plan์ ์ ํํ๋ค
๋งจ ์๋ launch web service๋ฅผ ํด๋ฆญํ๋ค
๊ฑฐ์ ๋ค์๋ค!
7. ai-plugin.json, openapi.yaml ์์ฑํ๊ธฐ
render์ ํธ์คํ ์ ์ํค๊ณ ๋์, ๋น์ ์ ์น์๋น์ค๊ฐ ํธ์คํ ๋๊ณ ์๋ ๋งํฌ์ /ai-plugin.json ์ ๋ถ์ธ ๋ค์ ์ํฐ๋ฅผ ๋๋ฅด์
๋ณธ์ธ์ ai-plugin.json ํ์ผ์ ๋ด์ฉ์ด ๋ณด์ธ๋ค๋ฉด ์ฑ๊ณต์ ์ผ๋ก ํธ์คํ ๋์๋ค๋ ๋ป์ด๋ค
ai-plugin.json์์, ๋ค์์ ์ค์ ๋น์ ์ ์น์ฌ์ดํธ์ ํจ๊ป ์์ ํ์
"api": {
"type": "openapi",
"url": "(์น์ฌ์ดํธ ๋งํฌ)/.well-known/openapi.yaml",
"is_user_authenticated": false
},
"logo_url": "(์น์ฌ์ดํธ ๋งํฌ)/.well-known/logo.png",
openapi.yaml์์, ๋ค์์ ์ค์ ๋น์ ์ ์น์ฌ์ดํธ์ ํจ๊ป ์์ ํ์
version: 'v1'
servers:
- url: (์น์ฌ์ดํธ ๋งํฌ)
legal_info_url์ ๊ฒฝ์ฐ,
ํ๋ฌ๊ทธ์ธ์ ๋ฒ์ ์ ๋ณด๋ฅผ ์ ์ด ๋์ ์น์ฌ์ดํธ๊ฐ ํ์ํ๋ค
์ด๋ฒ์๋ ๋ฌด๋ฃ ํธ์คํ ์ฌ์ดํธ๋ฅผ ์ธ ๊ฒ์ด๋ค
๋ณธ๋ฌธ์์ github pages๋ฅผ ์ด์ฉํ๊ฒ ๋ค
๋ฒ๋ฅ ์ ์ธ ๋ด์ฉ์ ์ ์ด ๋๋ ๋ถ๋ถ์ด๊ธฐ ๋๋ฌธ์ ์ ๋ชจ๋ฅด๋ฉด ๊ทธ๋ฅ GPT์๊ฒ ์ ์ด๋ฌ๋ผ๊ณ ํ์
index.html์ ๋ง๋ค์ด github pages์ฉ repository์ ์ ์ฅํ๋ค
ai-plugin.json์ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ค
"legal_info_url": "<github pages ๋งํฌ>"
contact email ์ ๋ ๊ฒ๋ ๋นผ๋จน์ง ๋ง์
8. ๋ด๊ฐ ๋ง๋ ํ๋ฌ๊ทธ์ธ ์ค์นํ๊ธฐ
์ด์ ๋ด๊ฐ ๋ง๋ ํ๋ฌ๊ทธ์ธ์ด ์ ์๋ํ๋์ง ์ํํด๋ณผ ์ฐจ๋ก๋ค
plugin store๋ฅผ ์ด๊ณ , ์ฐ์ธก ํ๋จ 'Develop your own plugin' ๋ฒํผ์ ๋๋ฅธ๋ค
ํ๋ฌ๊ทธ์ธ์ ํธ์คํ ํ๋ ์น์ฌ์ดํธ ์ฃผ์ ๋งํฌ๋ฅผ ์ ๋๋ค
ai-plugin.json ํ์ผ๊ณผ openapi.yaml ํ์ผ์ ์ค๋ฅ๊ฐ ๋ฌ๋ค๋ฉด, ๋ฌด์์ธ์ง ํ์ธํ๊ณ ํด๊ฒฐํ๋ค
์ด์ install for me๋ฅผ ๋๋ฌ ์ค์นํ๋ฉด ๋์ด๋ค!
FAQ
Q. openai.yaml์ด ๋ถ๋ช ์๋๋ฐ ์๋ค๊ณ ๋ ์
A. openapi ์ ๋๋ค
Q. api ๊ณต์๋ฌธ์๊ฐ ๋๋ฌด ์ปค์ GPT์๊ฒ ์ ์กํ ์๊ฐ ์์ด์
A. apiํค๋ฅผ ๋ฐ๊ธ ๋ฐ์ ์ฌ๊ธฐ์ ์ ๋ ฅํ, ์ต๋ ํ ํฐ ๊ธธ์ด๋ฅผ ์ค์ ํ ๋ํํด๋ณด์ธ์ (์ด๊ฑด ์ฌ์ฉ๋ฃ ์์)
๋๋, https://github.com/LagPixelLOL/ChatGPTCLIBot ์ด๊ฑธ ์ด์ฉํด๋ณด์ธ์ (๋๊ธ์ ์ํ๋ ์ฌ๋์์ผ๋ฉด ์ฌ์ฉ๋ฒ ์ฌ๋ฆฌ๊ฒ ์)
๋๊ธ์ ์ง๋ฌธ ๋ฐ์
์ ์ด๋ ๊ฒ ๋ณต์กํ ๊ณผ์ ๋ง๊ณ "์ผ, ์๊ฐ๋น 10๋ง์ ๋ฒ๋ ํ๋ฌ๊ทธ์ธ ๋ง๋ค์ด" ํ๋ฉด ๋๋ฑ ๋ง๋๋ AI ์ํ๋ค๊ณ ใ ใ ใ ํด์ค
์ค๋ช ์ด ์ข ์ด๋ ค์ ๋? ํ๋กฌํํธ๋ง ์ ์ผ๋ฉด ๋๋๊ฑด๋ฐ...
ํ๋กฌํํธ๋ง????? ๊ฑ ๋๋น๋ง์ผ๋ก๋ "ํด์ค"
๋๋ด์ด๊ณ ์ข์๊ธ ๊ฐ์
์ง์ง ์ ์ฑ์ค๋ ์ด ์ ๋ณด๊ธ๊ฐ์๋ฐ ๋๋ด์ด๋ผ๋ ์ด์ฌ๋์ ในใ ๊ธฐ๋ถ๋์๊ฒ ๋ค
์ข์ ๊ธ. ๊ฐ์ถ.