打造GitHub文档机器人

我仓库中的所有文档都是由GPT4生成的,我可以通过点击按钮来更新这些文档。

打造GitHub文档机器人

自动化是由AgentHub驱动的,它能够读取我的GitHub项目、生成新的Markdown文档,并为我发布PR。以下是我是如何制作它的以及你可以如何构建自己的。

为什么?

试图保持文档是最新的就像在潮汐线下建造沙堡一样。你可能想尽办法完善它,但贡献的浪潮最终会冲毁它,使其变成毫无意义的泥浆。这在活跃的开源社区中尤其如此。

“被潮汐冲刷后的沙堡遗迹”由Midjourney创作 🎨

这种艺术的本质不在于其持久性,而在于其短暂的存在,提醒我们就像日落一样,真正的美常常存在于短暂之中。——ChatGPT

让我们忽略这个比喻中的短暂之美,承认重写文档是令人沮丧的。它可能会让人感到如此沮丧,以至于许多项目忽略了它,让文档退化到需要完全重写的地步。即使他们确实分配了时间,一个足够了解编写文档的工程师也不应该花费宝贵的时间去编写它们。

我在AgentHub上建立了一个由LLM驱动的管道,用于为我的仓库生成文档。我的沙堡会自动重建。

1、工作原理

每次运行管道时,它都会摄取我的GitHub仓库中的所有文件,通过LLM请求生成描述性的Markdown文档,最后使用我的GitHub凭据提出PR。

为了提供背景信息,我正在记录的代码是AgentHub操作员的Python代码。这些是构成AgentHub流水线的基本模块(称为操作员)。每个操作员是一个简短的Python文件,旨在完成特定任务(例如:HackerNews抓取HN帖子,IngestPDF通过并输出为纯文本)。

这个解决方案是使用操作员构建的,同时也记录了相同的操作员。希望这不会太令人困惑。

这是我在AgentHub上的流水线:

2、从GitHub读取文件

首先,我需要从GitHub抓取文件的内容。为此,我使用了“从GitHub读取文件”操作符。它返回文件名以及以纯文本形式的文件内容。

def read_github_files(self, params, ai_context):  
    repo_name = params['repo_name']  
    folders = params.get('folders').replace(" ", "").split(',')  
    file_regex = params.get('file_regex')  
    branch = params.get('branch', 'master')  
  
    g = Github(ai_context.get_secret('github_access_token'))  
    repo = g.get_repo(repo_name)  
  
    file_names = []  
    file_contents = []  
  
    def file_matches_regex(file_path, file_regex):  
        if not file_regex:  
            return True  
  
        return re.fullmatch(file_regex, file_path)  
  
    def bfs_fetch_files(folder_path):  
        queue = [folder_path]  
  
        while queue:  
            current_folder = queue.pop(0)  
  
            contents = repo.get_contents(current_folder, ref=branch)  
  
            for item in contents:  
                if item.type == "file" and file_matches_regex(item.path, file_regex):  
                    file_content = item.decoded_content.decode('utf-8')  
                    file_names.append(item.path)  
                    file_contents.append(file_content)  
  
                elif item.type == "dir":  
                    queue.append(item.path)  
  
    for folder_path in folders:  
        bfs_fetch_files(folder_path)  
  
    ai_context.add_to_log(f"{self.declare_name()} Fetched {len(file_names)} files from GitHub repo {repo_name}:\n\r{file_names}", color='blue', save=True)  
  
    ai_context.set_output('file_names', file_names, self)  
    ai_context.set_output('file_contents', file_contents, self)  
    return True

3、请求ChatGPT

这是流水线的核心部分。我将前一步骤中每个文件的内容作为上下文传递给这个“请求ChatGPT”操作符。我使用以下非常明确的提示来生成Markdown文档。

为这段代码生成Markdown文档。这份文档旨在总结该操作员的功能和技术细节。

使用标题将文档分解成以下部分。

概述:对该操作员功能的一句话总结。

输入:简要描述输入及其用途

参数:简要描述参数及其用途

输出:简要描述输出

功能:本节总结run_step和辅助函数。
def run_step(self, step, ai_context):  
    p = step['parameters']  
    question = p.get('question') or ai_context.get_input('question', self)  
    # We want to take context both from parameter and input.  
    input_context = ai_context.get_input('context', self)  
    parameter_context = p.get('context')  
      
    context = ''  
    if input_context:  
        context += f'[{input_context}]'  
      
    if parameter_context:  
        context += f'[{parameter_context}]'  
  
    if context:  
        question = f'Given the context: {context}, answer the question or complete the following task: {question}'  
  
    ai_response = ai_context.run_chat_completion(prompt=question)  
    ai_context.set_output('chatgpt_response', ai_response, self)  
    ai_context.add_to_log(f'Response from ChatGPT: {ai_response}', save=True)

(源代码)

4、创建拉取请求

最后,如果我必须手动复制输出并提出PR,这种自动化将是一项繁琐的工作。这最后一步将文件名列表和文件内容(Markdown文档)转换为我的账户并通过我的账户创建PR。

def run_step(
    self, 
    step, 
    ai_context : AiContext
  ):
    params = step['parameters']
    file_names = ai_context.get_input('file_names', self)
    file_contents = ai_context.get_input('file_contents', self)
  
    g = Github(ai_context.get_secret('github_access_token'))
    repo = g.get_repo(params['repo_name'])
    forked_repo = repo.create_fork()
  
    base_branch_name = 'main'
    base_branch = repo.get_branch(base_branch_name)
  
    all_files = []
    contents = repo.get_contents("")
    while contents:
        file_content = contents.pop(0)
        if file_content.type == "dir":
            contents.extend(repo.get_contents(file_content.path))
        else:
            file = file_content
            all_files.append(str(file).replace('ContentFile(path="','').replace('")',''))
  
  
    new_branch_name = f"agent_hub_{ai_context.get_run_id()}"
    GitHubDocsWriter.create_branch_with_backoff(forked_repo, new_branch_name, base_branch.commit.sha)
  
    run_url = f'https://agenthub.dev/agent?run_id={ai_context.get_run_id()}'
  
    for file_name, file_content_string in zip(file_names, file_contents):
        file_path = file_name
        name = os.path.splitext(os.path.basename(file_path))[0] + '.md'
        docs_file_name = params['docs_folder_name'] + '/' + name
  
        commit_message = f"{file_path} - commit created by {run_url}"
  
        if docs_file_name in all_files:
            file = repo.get_contents(docs_file_name, ref=base_branch_name)
            forked_repo.update_file(docs_file_name, commit_message, file_content_string.encode("utf-8"), file.sha, branch=new_branch_name)
        else:
            forked_repo.create_file(docs_file_name, commit_message, file_content_string.encode("utf-8"), branch=new_branch_name)
  
    # Create a pull request to merge the new branch in the forked repository into the original branch
    pr_title = f"PR created by {run_url}"
    pr_body = f"PR created by {run_url}"
  
    pr = repo.create_pull(
        title=pr_title, 
        body=pr_body, 
        base=base_branch_name, 
        head=f"{forked_repo.owner.login}:{new_branch_name}"
    )
    
    ai_context.add_to_log(f"Pull request created: {pr.html_url}")

@staticmethod  
def create_branch_with_backoff(forked_repo, new_branch_name, base_branch_sha, max_retries=3, initial_delay=5):
    delay = initial_delay
    retries = 0

    while retries < max_retries:
        try:
            forked_repo.create_git_ref(ref=f"refs/heads/{new_branch_name}", sha=base_branch_sha)
            return
        except Exception as e:
            if retries == max_retries - 1:
                raise e

            sleep_time = delay * (2 ** retries) + random.uniform(0, 0.1 * delay)
            print(f"Error creating branch. Retrying in {sleep_time:.2f} seconds. Error: {e}")
            time.sleep(sleep_time)
            retries += 1

5、GPT 3.5 vs. GPT 4

AgentHub 允许你选择要在流水线中运行的模型。我尝试了 GPT-3.5-turbo 和 GPT4 来完成这个任务。GPT4 的文档留下的想象空间更少,更接近于我想要写的。

以下是一个例子,展示了每个模型对同样相对复杂的操作符进行文档处理的结果。

5.1 GPT3.5 的尝试

5.2 GPT4 的尝试

6、结束语

如果你有任何想法或需求的新操作符,我很乐意帮助你!

我构建了 AgentHub,并且很乐意听取你希望自动化的任何任务。请随时加入 discord,并关注(非常新的)AgentHub Twitter

我要特别感谢 Sam Snodgrass 提出了这个自动化方案,并为 AgentHub 贡献了操作符!

最后,如果你觉得这很有趣,我建议阅读 我上一篇文章。我使用 LLMs 自动化了一个进口公司的订单台。

谢谢你的阅读。


原文链接:Creating a GitHub Documentation Bot With LLMs

汇智网翻译整理,转载请标明出处