我的博客
用 GPTs Actions 搭建一个 Github 源码阅读助手

用 GPTs Actions 搭建一个 Github 源码阅读助手

Code Reader 体验地址:https://chat.openai.com/g/g-c1J88bC63-code-reader (opens in a new tab)
Coder Reader Github 地址:https://github.com/sdaaron/github-reader (opens in a new tab)

11.12 更新:做了一个 Bot Studio 版的

可以直接在 Bot Studio 大本营群里搜索使用,也可以扫描下方二维码体验: [图片]

背景介绍

对不少程序员来说,学习一个新的开源项目是一件挺有难度的事。知名开源项目往往文件众多、结构复杂,往往让人在初上手时一头雾水,不知从何开始。

自动有了 ChatGPT,相信很多人在某个时刻都产生过这个想法:「 什么时候 ChatGPT 能够帮我一次性阅读整个源码仓库就好了!」

现在,依靠 GPT-4-Turbo 128K 的超长上下文长度和 GPTs 给 ChatGPT 赋予的可定制化能力,我们终于可以实现这个想法了。

另外,相信很多人现在对 GPTs 和 GPT Assistant 的印象是这样的:

  1. GPTs 是给非程序员用的,只能通过 prompt 定义,最多勾选几项能力。可定制化程度低。
  2. GPT Assisant 是给专业程序员用的,高度可定制化。

其实不然,本文将主要介绍一个奥特曼在发布会上没有讲的一个小细节:GPTs Actions,这项功能让 GPTs 也拥有了等效于函数调用的全部能力。让 GPTs 也成为了一个可以调用任何外部工具的开放性 Agent !

成品体验

有 GPTs 访问权限的可以通过 https://chat.openai.com/g/g-c1J88bC63-code-reader (opens in a new tab) 在线体验 Code Reader。

(注意:128K 的窗口让 GPT4-Turbo 可以阅读大多数 Github 仓库,但是仍然有很多仓库的长度远远超过 128K token 限制,甚至长到会超过 web 服务器所能接受的上限,所以尽量不要用 Coder Reader 去读那些过于大的项目。)

暂时无法访问 GPTs 的可以通过以下截图预览下 Code Reader 的使用体验: [图片] [图片] [图片] [图片] [图片] 太酷了,现在 GPT 可以阅读整个项目源码,学习开源项目变得前所未有地简单!

搭建过程

  1. 进入 ChatGPT 官方 Web 界面,如果你是 ChatGPT Plus 用户,此时应该已经可以看到 GPTs 的入口「 Explore 」: [图片]

  2. 进入「 Explore 」,点击「 Create a GPT 」,即可看到山姆奥特曼在发布会上显示的界面: [图片]

  3. 进入「 Configure 」,我们通过代码配置的方式进入这个 GPT 的开发,会更便捷一些: [图片] 这个界面左边是开发区,右边是预览区,开发完即可在预览区测试,非常方便。

  4. 填入你的 GPT 的名称、描述、指令、和对话开头。以下是我填的信息: [图片]

  5. 给我们的私人定制 GPT 接上外部工具: [图片] 这里有几种工具可以选:

  6. 知识库

  7. OpenAI 内置的三大能力:网页浏览、DallE3 画图、Code Interpreter 执行代码

  8. Actions 这里我知识库和内置的三大能力都没有勾选,因为用不上,我们这个源码阅读助手的关键在于需要能让 GPT 访问到 github 仓库的源码,这需要 Actions 实现。所以接下来我们进入 Actions 开发。

  9. 开发 Actions: [图片]

所谓的 Actions, 其实也可以理解为函数调用(Function Calling)的一种具体形式。与函数调用不同的是, Actions 仅限于 GET/ POST/ PUT/ DELETE 等有限动作,不像函数调用那样可以调用任意本地函数。当然,这也是理所应当的,毕竟 OpenAI 也没有那么多计算资源可以让所有人在他们的硬件上跑云函数。

不过,能够执行 GET/ POST/ PUT/ DELETE 也完全够了,能够调用外部 API 就等于给 GPT 接上了无限可能。这里我们仅仅用一个获取 Github 仓库源码的 API 做示范,给 GPT 接上这个能力。

为了告诉 GPT 怎么调用 API,需要定义一个 Schema。这里 OpenAI 提供了一些实例,这里我们用 OpenAI Profile 这个模版修改一下得到一个能够往请求体中传入 git_url 参数的 Schema。 [图片] 修改完成后的 Schema 如下:

{
  "openapi": "3.1.0",
  "info": {
    "title": "Get Repository Content",
    "description": "get the source code content of an entire github repository.",
    "version": "v0.0.1"
  },
  "servers": [
    {
      "url": "https://github-reader.onrender.com"
    }
  ],
  "paths": {
    "/get-repo-content/": {
      "post": {
        "description": "get the source code content of an entire github repository.",
        "operationId": "Get Repository Content",
        "parameters": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Get Repository ContentRequestSchema"
              }
            }
          },
          "required": true
        },
        "deprecated": false,
        "x-openai-isConsequential": true
      }
    }
  },
  "components": {
    "schemas": {
      "Get Repository ContentRequestSchema": {
        "properties": {
          "git_url": {
            "type": "",
            "title": "git_url",
            "description": "get the github repository url from user\u2019s input"
          }
        },
        "type": "object",
        "required": ["git_url"],
        "title": "Get Repository ContentRequestSchema"
      }
    }
  }
}

在这个 Schema 里面,我们需要自己定义的就是 Action 的名称、描述、服务器 url,PATH,每个 PATH 的描述和需要传入的参数。

(我 11 月 9 日第一次尝试开发一个 GPTs 的时候,OpenAI 官方界面上还有一个可视化的配置界面可以配置这个 Schema,所以第一次配的时候非常简单。但是截止到 11 月 10 日,这个界面已经迅速下线了,现在只能够通过 Schema 文件配置,非常迷惑,可能 OpenAI 发现了什么 Bug 临时下线了,之后大概还会恢复回来)

  1. 开发一个 API 来获取 Github 仓库的源代码 我们已经告诉 GPT 应该通过向 https://github-reader.onrender.com (opens in a new tab) POST 一个含有 git_url 的请求体,来获取某个 Github 仓库的源代码,但是目前还并不存在这样一个 API 可用,我们需要自己开发。

用 FastAPI 来搭建这样一个简单 API 非常简单,只需要 80 行代码,让 GPT 帮你写,1 分钟就搞定了。

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import os
import shutil
import asyncio
from git import Repo
 
app = FastAPI()
 
class GitRepo(BaseModel):
git_url: str
 
@app.get("/")
async def get_main():
return {"message": "Welcome to Code Reader!"}
 
@app.post("/get-repo-content/")
async def print_repo_url(repo: GitRepo):
git_url = repo.git_url
try:
repo_name = git_url.split("/")[-1]
repo_name = (
repo_name.replace(".git", "") if repo_name.endswith(".git") else repo_name
)
 
        temp_dir = f"./temp_{repo_name}"
        if os.path.exists(temp_dir):
            shutil.rmtree(temp_dir)
        os.makedirs(temp_dir)
 
        repo_dir = os.path.join(temp_dir, repo_name)
        print("start cloning")
        # 异步克隆仓库
        await clone_repo_async(git_url, repo_dir)
 
        print("start reading")
        # 异步读取所有文件
        content = await read_all_files_async(repo_dir)
        print("read finished. conetent length: ", len(content))
        # 确保在此处删除临时目录
        shutil.rmtree(temp_dir)
        print("temp dir removed")
        return {"content": content[:50000]}
 
    except Exception as e:
        # 如果出现异常,也应该清理临时目录
        if os.path.exists(temp_dir):
            shutil.rmtree(temp_dir)
        raise HTTPException(status_code=500, detail=str(e))
 
async def clone_repo_async(git_url, repo_dir):
loop = asyncio.get_event_loop()
print(f"开始克隆仓库: {git_url}")
await loop.run_in_executor(
None, lambda: Repo.clone_from(git_url, repo_dir, depth=1)
)
print(f"仓库克隆完成: {repo_dir}")
 
async def read_all_files_async(directory):
loop = asyncio.get_event_loop()
print(f"开始读取文件: {directory}")
content = await loop.run_in_executor(None, lambda: read_all_files(directory))
print(f"文件读取完成. 总字符数: {len(content)}")
return content
 
def read_all_files(directory):
all_text = ""
for root, dirs, files in os.walk(directory):
for file_name in files:
file_path = os.path.join(root, file_name)
if os.path.isfile(file_path):
try:
with open(file_path, "r", encoding="utf-8") as file:
all_text += f"File: {file_name}\n\n" + file.read() + "\n\n"
except UnicodeDecodeError:
print(f"无法以 UTF-8 编码读取文件: {file_path}")
except Exception as e:
print(f"读取文件时发生错误: {file_path}, 错误: {e}")
return all_text
 
if **name** == "**main**":
import uvicorn
 
    uvicorn.run("main:app", host="0.0.0.0", port=80)
  1. 部署 API 服务 这个服务总共就 80 行代码,为了这点代码整一台云服务器部署实在太麻烦,所以这里我用了一个后端托管服务:render,将这个小脚本托管成了一个 web 后端服务。具体部署方法我就不浪费口舌了,可以参考https://render.com/docs。或者用其他自己熟悉的方式部署也行。 (opens in a new tab)

完整的 render 部署代码已经上传到 https://github.com/sdaaron/github-reader (opens in a new tab) 了。需要用的话可以叉走一份。

  1. 大功告成! 现在你的 API 已经就绪,GPTs 也开发完毕,快测试一下 GPT 是否能正确调用 API 吧!

如果成功,那么你已经掌握了一个能执行你自定义动作的私人 GPT 了!