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

Code Reader 体验地址https://chat.openai.com/g/g-c1J88bC63-code-reader

Coder Reader Github 地址https://github.com/sdaaron/github-reader

11.12更新

做了一个 Bot Studio 版的,可以直接在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 在线体验Code Reader。

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

暂时无法访问GPTs的可以通过以下截图预览下 Code Reader 的使用体验:

Code Reader 使用体验截图 Code Reader 使用体验截图 Code Reader 使用体验截图 Code Reader 使用体验截图 Code Reader 使用体验截图

太酷了,现在GPT可以阅读整个项目源码,学习开源项目变得前所未有地简单!

搭建过程

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

GPTs Explore 入口

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

Create a GPT 界面

3. 进入「 Configure 」,我们通过代码配置的方式进入这个GPT的开发,会更便捷一些:

Configure 配置界面

这个界面左边是开发区,右边是预览区,开发完即可在预览区测试,非常方便。

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

GPT 基本设置信息

5. 给我们的私人定制GPT接上外部工具:

外部工具配置

这里有几种工具可以选:

  1. 知识库
  2. OpenAI 内置的三大能力:网页浏览、DallE3 画图、Code Interpreter执行代码
  3. Actions

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

6. 开发 Actions:

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 模板配置

修改完成后的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's 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临时下线了,之后大概还会恢复回来)

7. 开发一个 API 来获取Github 仓库的源代码

我们已经告诉GPT应该通过向 https://github-reader.onrender.com 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)

8. 部署 API 服务

这个服务总共就80行代码,为了这点代码整一台云服务器部署实在太麻烦,所以这里我用了一个后端托管服务:render,将这个小脚本托管成了一个web后端服务。具体部署方法我就不浪费口舌了,可以参考https://render.com/docs。或者用其他自己熟悉的方式部署也行。

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

9. 大功告成!

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

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