掘金 后端 ( ) • 2024-04-20 15:56

本文发表于入职啦(公众号: ruzhila) 大家可以访问入职啦学习更多的编程实战。

代码已经开源: fgpt 欢迎大家star⭐和fork

先看看今天达到的效果:

动图封面

接着上一篇文章,我们继续实现fgpt命令行工具,之前已经实现了根据命令行参数返回ChatGPT的结果,这次我们要实现更多的功能,比如支持文件输入、交互式输入等。

Shell的管道输入

Unix/Linux 的Shell是非常强大的,它支持管道输入,比如下面的命令:

git diff | fgpt "Write a git commit bief with follow diff"

这个命令,相当于把git diff的输出作为输入,传递给fgpt,然后fgpt返回一个git commit的描述。

根据之前的代码实现,是支持多段消息输入的方式, 从程序的数据流程上规划了多段消息的组成:

  • 第一部分: 作为系统提示, 在后续的Code模式上可以起作用,就是给ChatGPT设计一个角色
  • 第二部分: 作为用户输入, 主要是是命令行的输入, 也就是Write a git commit bief ... 这段文本
  • 第三部分: 作为文件或者管道输入, 也就是git diff的输出

对应就是要拼出一个这样的数据结构:

[
    {
        "role": "system",
        "message": "Your are a developer", // 根据参数来确定
    },
    {
        "role": "user",
        "message": "Write a git commit bief with follow diff" // 从命令行输入
    },
    {
        "role": "user",
        "message": "......" // 从文件或者管道输入
    }
]

Code模式的实现

fgpt中,我们可以通过-c或者--code参数来指定Code模式,这个模式下,fgpt返回的是一段代码,而不是一段文本。

为了实现这个效果,我们需要在会话的第一部分添加一个Prompt, 具体可以见src/role.code.prompt的内容,这个文件会通过include_str!这个宏编译到代码中:

let mut messages = vec![];
    if state.code {
        messages.push(Message {
            role: "system".to_string(),
            content: include_str!("./role.code.prompt").to_string(),
            content_type: "text".to_string(),
        });
    }

这样可以确保在Code模式下,会话的第一部分是一个角色设定,加上用户的输入,就可以返回一段代码了:

mpi@mpis-Mac-mini fgpt % fgpt -c "Write python code to reverse a string"
def reverse_string(s):  
    return s[::-1]    
# Example usage:       
# original_string = "hello" 
# reversed_string = reverse_string(original_string)
# print(reversed_string) 

管道和文件输入

Linux的程序在运行过程中,要识别输入的来源,是来自于文件、管道还是终端,这个是通过stdin来实现的,可以通过std::io::stdin().is_terminal()来判断当前是管道还是终端

如果是管道输入,我们需要读取stdin的内容作为第三部分的输入:

if !std::io::stdin().is_terminal() {
        // it may be a pipe or a file
        let mut content = String::new();
        std::io::stdin().read_to_string(&mut content)?;
        messages.push(Message {
            role: "user".to_string(),
            content,
            content_type: "text".to_string(),
        });
    }

交互式输入

交互式最典型的就是Python的REPL模式,用户可以输入一段代码,然后返回结果,这个模式在fgpt中也是支持的,只需要不带任何参数就可以进入交互式模式。

要实现这个效果,那么就需要依赖一个readline类似的库:rustyline, 这个库可以非常方便的实现交互式输入,比如下面的代码:

let mut rl = rustyline::Editor::<()>::new();
    loop {
        let readline = rl.readline(">> ");
        match readline {
            Ok(line) => {
                if line.is_empty() {
                    continue;
                }
                rl.add_history_entry(line.as_str());
                messages.push(Message {
                    role: "user".to_string(),
                    content: line,
                    content_type: "text".to_string(),
                });
            }
            Err(_) => break,
        }
    }

实现对话效果

根据Web API的协议规范,在同个device_id下,接最后一条消息的conversation_idmessage_id就可以完成对话的效果, 只需要记录最后一条消息的conversation_idmessage_id就可以了。

let mut conversation_id = None;
    let mut message_id = None;
    for message in messages.iter() {
        conversation_id = Some(message.conversation_id.clone());
        message_id = Some(message.message_id.clone());
    }

    // next request 
    let req = CompletionRequest {
        conversation_id,
        parent_message_id: message_id,
        ....
    };

总结

当前这个版本已经能支持对话和交互式输入了,下一步我们要实现一个Reverse Proxy的功能

但是还有很多优化空间,特别是配色的支持,还有要支持输出实时预览的,可以将输出的结果带颜色的输出到终端

我是一个写了20多年代码的老程序员,如果大家想学习编程,可以关注公众号:入职啦,我会分享更多的编程实战经验。