学习如何构建一个简单的OAuth待办事项插件。
要创建一个OAuth插件,我们首先定义一个ai-plugin.json文件,并将auth类型设置为oauth:
```json
{
"schema_version": "v1",
"name_for_human": "TODO List (OAuth)",
"name_for_model": "todo_oauth",
"description_for_human": "Manage your 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": "oauth",
"client_url": "PLUGIN_HOSTNAME/oauth",
"scope": "",
"authorization_url": "PLUGIN_HOSTNAME/auth/oauth_exchange",
"authorization_content_type": "application/json",
"verification_tokens": {
"openai": "Replace_this_string_with_the_verification_token_generated_in_the_ChatGPT_UI"
}
},
"api": {
"type": "openapi",
"url": "PLUGIN_HOSTNAME/openapi.yaml"
},
"logo_url": "PLUGIN_HOSTNAME/logo.png",
"contact_email": "contact@example.com",
"legal_info_url": "http://www.example.com/legal"
}
```
接下来,我们需要定义我们的OAuth服务。此OAuth示例并不适用于生产环境,而是为了突出简单的OAuth流程,以便开发者可以获得构建生产解决方案的经验。
```python
import jsonimport quart
import quart_cors
from quart import requestapp = quart_cors.cors(quart.Quart(__name__), allow_origin="*")
_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):
print(request.headers)
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"]
# fail silently, it's a simple plugin
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("manifest.json") as f:
text = f.read()
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()
text = text.replace("PLUGIN_HOSTNAME", f"https://{host}")
return quart.Response(text, mimetype="text/yaml")@app.get("/oauth")
async def oauth():
query_string = request.query_string.decode('utf-8')
parts = query_string.split('&')
kvps = {}
for part in parts:
k, v = part.split('=')
v = v.replace("%2F", "/").replace("%3A", ":")
kvps[k] = v
print("OAuth key value pairs from the ChatGPT Request: ", kvps)
url = kvps["redirect_uri"] + f"?code={OPENAI_CODE}"
print("URL: ", url)
return quart.Response(
f'<a href="{url}">Click to authorize</a>'
)# Sample names
OPENAI_CLIENT_ID = "id"
OPENAI_CLIENT_SECRET = "secret"
OPENAI_CODE = "abc123"
OPENAI_TOKEN = "def456"@app.post("/auth/oauth_exchange")
async def oauth_exchange():
request = await quart.request.get_json(force=True)
print(f"oauth_exchange {request=}")if request["client_id"] != OPENAI_CLIENT_ID:
raise RuntimeError("bad client ID")
if request["client_secret"] != OPENAI_CLIENT_SECRET:
raise RuntimeError("bad client secret")
if request["code"] != OPENAI_CODE:
raise RuntimeError("bad code")return {
"access_token": OPENAI_TOKEN,
"token_type": "bearer"
}def main():
app.run(debug=True, host="0.0.0.0", port=5002)if __name__ == "__main__":
main()
```
最后,与我们的其他示例一样,我们基于端点定义一个简单的OpenAPI文件:
openapi: 3.0.1
info:
title: TODO Plugin
description: A plugin that allows the user to create and manage a TODO list using ChatGPT. If you do not know the user's username, ask them first before making queries to the plugin. Otherwise, use the username "global".
version: "v1"
servers:
- url: PLUGIN_HOSTNAME
paths:
/todos/{username}:
get:
operationId: getTodos
summary: Get the list of todos
parameters:
- in: path
name: username
schema:
type: string
required: true
description: The name of the user.
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/getTodosResponse"
post:
operationId: addTodo
summary: Add a todo to the list
parameters:
- in: path
name: username
schema:
type: string
required: true
description: The name of the user.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/addTodoRequest"
responses:
"200":
description: OK
delete:
operationId: deleteTodo
summary: Delete a todo from the list
parameters:
- in: path
name: username
schema:
type: string
required: true
description: The name of the user.
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/deleteTodoRequest"
responses:
"200":
description: OKcomponents:
schemas:
getTodosResponse:
type: object
properties:
todos:
type: array
items:
type: string
description: The list of todos.
addTodoRequest:
type: object
required:
- todo
properties:
todo:
type: string
description: The todo to add to the list.
required: true
deleteTodoRequest:
type: object
required:
- todo_idx
properties:
todo_idx:
type: integer
description: The index of the todo to delete.
required: true
评论