实战:ChatGPT Plugin 开发
ChatGPT Plugin 开发指南
插件可以上传到 openai的插件商店中使用,也可以在里面使用别人的插件
其原理就是让chatgpt 调用能够调用我们的服务
插件开发推荐步骤
要构建一个插件,理解端到端流程是很重要的。
1
创建一个清单文件并将其托管在yourdomain.com/.well-known/ai-plugin.json上
该文件包含有关插件的元数据(名称,徽标等),以及有关身份验证的详细信息
必需的(验证类型,OAuth url等),以及您想要的端点的OpenAPI规范
暴露。
该模型将看到OpenAPI描述字段,这可以用来提供一个自然的
不同字段的语言描述。
我们建议在开始时只暴露1-2个端点,数量最少
参数来最小化文本的长度。插件描述、API请求和API
使用ChatGPT将所有响应插入到对话中。这与上下文不符
模型的极限。
2在ChatGPT界面中注册插件
从顶部的下拉菜单中选择插件模型,然后选择“Plugins”,“plugin Store”,和
最后“开发你自己的插件”。
如果需要身份验证,则提供OAuth 2 client_id和client_secret或API
关键。
用户激活你的插件
用户必须在ChatGPT用户界面中手动激活插件。(ChatGPT不会使用您的插件
默认情况下)。
您将能够与100个其他用户共享您的插件(只有其他开发人员可以安装)
未经证实的插件)。
如果需要OAuth,用户将通过OAuth重定向到您的插件登录。
4用户开始对话
OpenAI将在ChatGPT的消息中注入插件的紧凑描述,对ChatGPT不可见
最终用户。这将包括插件描述、端点和示例。
当用户询问相关问题时,模型可能会选择从您的
插件;对于POST请求,我们要求开发人员构建一个用户
确认流程以避免破坏操作。
该模型将把API调用结果合并到对用户的响应中。
该模型可能在其响应中包含从API调用返回的链接。这些将是
显示为丰富的预览(使用OpenGraph协议,我们将site_name,title,
描述、图片和url字段)。
该模型还可以在markdown中格式化来自API的数据,并且ChatGPT UI将呈现
自动降价。以上翻译结果来自有道神经网络翻译(YNMT)· 通用场景
项目实战 Tolist
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: http://localhost:5003 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: OK components: 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
{ "schema_version": "v1", "name_for_human": "TODO List (no auth)", "name_for_model": "todo", "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": "none" }, "api": { "type": "openapi", "url": "http://localhost:5003/openapi.yaml" }, "logo_url": "http://localhost:5003/logo.png", "contact_email": "legal@example.com", "legal_info_url": "http://example.com/legal" }
import json import quart import quart_cors from quart import request app = quart_cors.cors(quart.Quart(__name__), allow_origin="https://chat.openai.com") # Keep track of todo's. Does not persist if Python session is restarted. _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"] # 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("./.well-known/ai-plugin.json") as f: text = f.read() 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() return quart.Response(text, mimetype="text/yaml") def main(): app.run(debug=True, host="0.0.0.0", port=5003) if __name__ == "__main__": main()
样例项目:待办(Todo)管理插件
实战:天气预报(Weather Forecast)插件开发
{ "schema_version": "v1", "name_for_human": "Weather Forecast", "name_for_model": "weather", "description_for_human": "Global Weather Forecast. You can ask the current or future weather of any city around the world.", "description_for_model": "Plugin for managing weather forecasts. Search current weather and future forecasts through provided api.", "auth": { "type": "none" }, "api": { "type": "openapi", "url": "http://localhost:5002/openapi.yaml" }, "logo_url": "http://localhost:5002/logo.png", "contact_email": "pjt73651@gmail.com", "legal_info_url": "http://example.com/legal" }
openapi: 3.0.1 info: title: Weather Forecast description: A Plugin that allows the user to forecast current or future weather using ChatGPT. Ask for clarification if a user request is ambiguous. version: 'v1' servers: - url: http://localhost:5002 paths: /weather/current: get: operationId: getCurrentWeather summary: Get the current weather of the city parameters: - in: query name: city schema: type: string required: true description: The city and state, e.g. San Francisco, CA. responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/getCurrentWeather' /weather/forecast: get: operationId: getNDayWeatherForecast summary: Forecast the weather of the city in a few days parameters: - in: query name: num_days schema: type: integer required: true description: The number of days to forecast, e.g. 5 - in: query name: city schema: type: string required: true description: The city and state, e.g. San Francisco, CA. responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/getNDayWeatherForecast' components: schemas: getCurrentWeather: type: object properties: weather: type: string description: The current weather of the city. getNDayWeatherForecast: type: object properties: weather: type: string description: The weather of the city in a few days.
# 导入 json 模块,用于 json 数据的序列化和反序列化 import json # 导入 os 模块,用于获取第三方天气平台 API_KEY import os # 导入 requests 模块,用于访问第三方天气平台 import requests # 导入 quart 模块,用于构建异步 Web 应用 import quart # 导入 quart_cors 模块,用于处理跨域资源共享 (CORS) import quart_cors # 从 quart 模块导入 request 对象,用于处理 HTTP 请求 from quart import request # 创建一个支持 CORS 的 Quart 应用实例,允许来自 "https://chat.openai.com" 的跨域请求 app = quart_cors.cors(quart.Quart(__name__), allow_origin="https://chat.openai.com") WEATHER_API_KEY = os.getenv("WEATHER_API_KEY") def get_citycode(city): url = "https://restapi.amap.com/v3/geocode/geo" params = { "city": city, "key": WEATHER_API_KEY, "address": city } try: response = requests.get(url, params=params) response.raise_for_status() # 从 response 中获取 citycode data = response.json() citycode = data["geocodes"][0]["adcode"] print(f"{city}: {citycode}") return citycode except requests.exceptions.RequestException as e: print(f"Error occurred during GET request: {e}") return None def _get_current_weather(city): citycode = get_citycode(city) url = "https://restapi.amap.com/v3/weather/weatherInfo" params = { "city": citycode, "key": WEATHER_API_KEY, } try: response = requests.get(url, params=params) response.raise_for_status() data = response.json() # 从 response 中提取天气相关信息 w = data["lives"][0] weather = f"今天{w['province']}{w['city']}天气{w['weather']},温度{w['temperature']}°C,湿度{w['humidity']}%,风向{w['winddirection']},风力{w['windpower']}。" return weather except requests.exceptions.RequestException as e: print(f"Error occurred during GET request: {e}") return None def _get_n_day_weather_forecast(city, num_days): if num_days > 3 or num_days < 0: return "最多查询未来3天的预报" citycode = get_citycode(city) url = "https://restapi.amap.com/v3/weather/weatherInfo" params = { "city": citycode, "key": WEATHER_API_KEY, "extensions": "all" } try: response = requests.get(url, params=params) response.raise_for_status() data = response.json() # 从 response 中提取天气相关信息 forecast = data["forecasts"][0]["casts"][num_days] date = forecast["date"] day_weather = forecast["dayweather"] night_weather = forecast["nightweather"] day_temp = forecast["daytemp"] night_temp = forecast["nighttemp"] day_wind = forecast["daywind"] night_wind = forecast["nightwind"] day_power = forecast["daypower"] night_power = forecast["nightpower"] weather = f"{date},白天天气{day_weather},夜晚天气{night_weather},白天温度{day_temp}°C,夜晚温度{night_temp}°C,白天风向{day_wind},夜晚风向{night_wind},白天风力{day_power},夜晚风力{night_power}。" return weather except requests.exceptions.RequestException as e: print(f"Error occurred during GET request: {e}") return None # 定义一个路由处理器,处理 GET 请求,路由为 "/weather/current" @app.get("/weather/current") async def get_current_weather(): # 从请求参数中获取城市名 city = request.args.get("city") response = _get_current_weather(city) # 创建并返回一个 Quart 响应对象,状态码为 200,响应体为 response 的 json 形式 return quart.Response(json.dumps(response), status=200) # 定义一个路由处理器,处理 GET 请求,路由为 "/weather/forecast" @app.get("/weather/forecast") async def get_n_day_weather_forecast(): # 从请求参数中获取城市名和预报天数 city = request.args.get("city") num_days = int(request.args.get("num_days")) response = _get_n_day_weather_forecast(city, num_days) # 创建并返回一个 Quart 响应对象,状态码为 200,响应体为 response 的 json 形式 return quart.Response(json.dumps(response), status=200) # 定义一个路由处理器,处理 GET 请求,路由为 "/logo.png" @app.get("/logo.png") async def plugin_logo(): # 文件名为 'weather-forecast.png' filename = 'weather-forecast.png' # 使用 quart.send_file 方法发送文件,并指定 MIME 类型为 'image/png' return await quart.send_file(filename, mimetype='image/png') # 定义一个路由处理器,处理 GET 请求,路由为 "/.well-known/ai-plugin.json" @app.get("/.well-known/ai-plugin.json") async def plugin_manifest(): # 从请求头中获取主机名 host = request.headers['Host'] # 打开并读取 ai-plugin.json 文件 with open("./.well-known/ai-plugin.json") as f: text = f.read() # 返回一个 Quart 响应对象,响应体为 text,MIME 类型为 "text/json" return quart.Response(text, mimetype="text/json") # 定义一个路由处理器,处理 GET 请求,路由为 "/openapi.yaml" @app.get("/openapi.yaml") async def openapi_spec(): # 从请求头中获取主机名 host = request.headers['Host'] # 打开并读取 openapi.yaml 文件 with open("openapi.yaml") as f: text = f.read() # 返回一个 Quart 响应对象,响应体为 text,MIME 类型为 "text/yaml" return quart.Response(text, mimetype="text/yaml") # 定义 main 函数,运行 Quart 应用 def main(): # 启动 Quart 服务器,开启 debug 模式,监听所有 IP 地址,端口为 5001 app.run(debug=True, host="0.0.0.0", port=5002) def test(): city = "上海" num_days = 2 weather_info = _get_current_weather(city) print(weather_info) weather_forecast = _get_n_day_weather_forecast(city, num_days) print(weather_forecast) # 如果该文件是直接运行的,而不是作为模块导入的,则调用 main 函数 if __name__ == "__main__": test() main()