写点什么

强强联合:Neovim+ChatGPT | 社区征文

作者:SkyFire
  • 2023-03-12
    美国
  • 本文字数:7854 字

    阅读完需:约 26 分钟

目录


Neovim 是一款备受开发者喜爱的文本编辑器,而 ChatGPT 则是 OpenAI 所推出的强大语言模型。这两者的结合可以带来令人惊叹的功能和效率,成为程序员们必备的工具之一。在本文中,我们将探讨 Neovim 和 ChatGPT 的强强联合,以及如何利用它们来提高编码和写作的体验。


要编写一个 ChatGPT 的 Neovim 插件,我的思路如下:


  1. 使用 python 调用 ChatGPT 的接口

  2. 在 vimscript 中异步调用 python,获取返回信息

  3. 在 vimscript 和 python 中 session 提供对连续对话的支持

ChatGPT 的 python 接口

接口文档参考:https://platform.openai.com/docs/guides/chat


示例代码如下:


import osimport openaiopenai.api_key = os.getenv("OPENAI_API_KEY")
completion = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "user", "content": "Hello!"} ])
print(completion.choices[0].message)
复制代码


这段 Python 代码导入了 osopenai 模块,使用 os.getenv() 方法设置了 OpenAI API 密钥,使用 openai.ChatCompletion.create() 方法创建了一个聊天完成对象,并打印出了模型的回复消息。


openai 模块用于与 OpenAI 的 GPT-3 语言模型 API 进行交互。os 模块用于从环境变量中检索 API 密钥。


openai.ChatCompletion.create() 方法接受两个参数:模型名称和消息列表。模型名称指定要使用哪个预训练模型生成响应。消息列表包含了对话历史,其中每条消息都是一个字典,包含一个 "role" 键和一个 "content" 键。在本例中,对话历史中只有一条用户消息,内容为 "Hello!"。


print(completion.choices[0].message) 语句检索了模型生成的回复消息,并将其打印到控制台上。completion.choices[0] 用于选择模型生成的第一个(也是唯一的)响应,.message 属性用于从响应中提取消息文本。

Neovim 异步执行 shell 命令

在 neovim 中,jobstart函数是用来启动一个异步的进程的。使用这个函数,你可以运行任何可执行的命令,并且在后台执行这个命令,而不会阻塞 neovim 的主线程。这样做的好处是,你可以在等待命令执行的同时继续进行你的编辑工作,而不必等待命令执行完成。


下面是一个使用jobstart函数执行 shell 命令的示例代码:


let job_id = jobstart(['your shell command'], {'on_stdout': function(job_id, data, is_last)  if is_last    call your_callback_function(data)  endifendfunction}, stdout_buffered: 1)
复制代码


在这个代码中,jobstart函数会异步地执行一个 shell 命令,并且返回一个唯一的job_id,你可以使用这个job_id来管理这个异步任务。on_stdout参数指定了一个回调函数,当命令的标准输出数据可用时,就会调用这个回调函数。data参数将包含命令的标准输出,is_last参数指示是否为最后一次调用该函数,因为可能有多个回调。


on_stdout回调函数中,你可以对输出的数据进行任何处理,最后,一个处理完整个任务的回调函数将会被调用(在本例中,我们称其为your_callback_function())。


其中,stdout_buffered选项表示是否缓存标准输出。如果该选项设置为 true,则标准输出将全部被缓存(在内存中),并且仅在任务完成时才通过回调函数on_exit返回。否则,标准输出会被逐行返回,因此on_stdout回调函数会被多次调用。

ChatGPT 连续对话

ChatGPT 连续对话的关键其实是每次都需要将以前对话的完整内容发送回服务器。文档中也给出了例子:


# Note: you need to be using OpenAI Python v0.27.0 for the code below to workimport openai
openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Who won the world series in 2020?"}, {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."}, {"role": "user", "content": "Where was it played?"} ])
复制代码


只需要将历史的对话信息在messages字段中回传服务器就可以实现连续对话了。


所以我们需要将每次调用的请求信息与响应信息存储一下,在下一次请求的时候带上。

插件开发

插件分为两个部分:


  1. python 文件,提供对 ChatGPT 的访问

  2. vimscript 文件,提供对 python 脚本的调用以及结果展示


下面我使用 ChatGPT 对具体的实现增加了中文注释。

python 部分

# 导入必要的模块 import osimport argparseimport json
# 尝试导入 openai try: import openaiexcept Exception as e: # 如果出现错误,使用 pip3 安装 openai os.system("pip3 install --user openai") import openai
# 存储 OpenAI API key 的文件keyFile = os.path.join(os.environ["HOME"], ".openai.key")
# 读取 OpenAI API keydef readOpenAiApiKey(): with open(keyFile, "r") as f: openai.api_key = f.read().strip()
# 聊天函数,使用 OpenAI 进行聊天def chat(model, content, session): # 如果没有设置 OpenAI API key,返回 "please put your apikey to %s" if openai.api_key is None: return "please put your apikey to %s" % keyFile # 创建消息 msg = {"role": "user", "content": content} # 如果使用了会话,从会话中读取历史消息 if session != "": try: with open(session, "r") as f: msgs = json.load(f) # 最多只保留 1000 条历史消息 if len(msgs) > 1000: msgs = msgs[:1000] msgs.append(msg) except Exception: msgs = [msg] else: msgs = [msg] try: # 使用 OpenAI 进行聊天,并获取回复消息 response_msg = openai.ChatCompletion.create(model=model, messages=msgs).choices[0].message except Exception as e: return e # 将回复消息添加到消息列表中 msgs.append(response_msg) # 如果使用了会话,将消息列表写入会话文件中 if session!= "": try: with open(session, "w") as f: json.dump(msgs, f) except Exception: pass # 返回回复消息的内容 return response_msg.content
# 主函数,解析命令行参数,执行聊天def main(): parser = argparse.ArgumentParser() # 命令行参数:OpenAI API key 存储路径 parser.add_argument("--keyfile", default=keyFile) # 命令行参数:使用的模型名称 parser.add_argument("--model", default="gpt-3.5-turbo") # 命令行参数:使用的会话文件路径 parser.add_argument("--session", default="") # 命令行参数:要发送的消息内容 parser.add_argument("content") result = parser.parse_args() # 如果 OpenAI API key 文件存在,读取 API key if os.path.exists(result.keyfile): readOpenAiApiKey() # 执行聊天,并输出回复消息的内容 print(chat(result.model,result.content, result.session))
# 如果当前文件作为主脚本运行,则执行主函数if __name__ == "__main__": main()

复制代码

vimscript 部分

" 设置chatgpt.py所在路径let g:chatgptPyScript = expand("<sfile>:p:h") . "/chatgpt.py"" 设置openai.key文件路径let g:openaiKeyFile = $HOME . "/.openai.key"" 设置chatgpt模型let g:chatgptModel = "gpt-3.5-turbo"" 当前聊天会话let g:currentSession = ""
" 打开聊天窗口,addheader参数默认为true,代表是否在窗口顶部添加插件信息function! chatgpt#OpenWindow(addheader=1) " 获取__chatgpt__缓冲区编号 let index = bufnr('__chatgpt__') let new = 0 " 如果缓冲区不存在,则新建一个左右分屏并打开__chatgpt__缓冲区 if index == -1 vsplit wincmd L enew file __chatgpt__ let index = bufnr('%') " 如果addheader为true,则在缓冲区末尾添加插件信息 if a:addheader == 1 call append(line('$'), '- ChatGPT Vim Plugin') call append(line('$'), '- SkyFire') call append(line('$'), '- https://github.com/skyfireitdiy/chatgpt') call append(line('$'), '- skyfireitdiy@hotmail.com') call append(line('$'), '-------------------------------------------------') endif let new = 1 else " 如果缓冲区存在,则先检查缓冲区是否在当前标签页中 " 如果不在,则新建一个左右分屏并打开该缓冲区,如果已存在则跳转到该窗口 if index(tabpagebuflist(), index) == -1 vsplit wincmd L execute 'buffer' index else call win_gotoid(win_findbuf(index)[0]) endif endif " 设置缓冲区局部选项 setlocal noswapfile setlocal hidden setlocal wrap setlocal filetype=markdown setlocal buftype=nofile return newendfunction
" 在聊天窗口中添加聊天内容function! chatgpt#addContent(content, addheader=1) " 打开聊天窗口并返回是否新建了窗口 let new = chatgpt#OpenWindow(a:addheader) " 将聊天内容添加到缓冲区末尾 call append(line('$'), a:content) " 获取当前会话对应的文件名,若当前为新建窗口,则清空缓冲区 let sessionFile = chatgpt#sessionFileName(g:currentSession) if new == 1 0delete endif " 如果会话文件名不为空,则写入会话文件 if sessionFile != "" execute "w! " . sessionFile endif " 光标跳转到文档末尾 normal! Gendfunction
" 清除聊天窗口function! chatgpt#wipeBuf() " 获取__chatgpt__缓冲区编号 let index = bufnr('__chatgpt__') " 如果缓冲区不存在,则返回 if index == -1 return endif " 如果缓冲区存在,则先检查缓冲区是否在当前标签页中 " 如果不在,则新建一个左右分屏并打开该缓冲区,如果已存在则跳转到该窗口 if index(tabpagebuflist(), index) == -1 vsplit execute 'buffer' index else call win_gotoid(win_findbuf(index)[0]) endif " 删除缓冲区 bwipeout!endfunction
" 处理Python进程的标准输出,将输出添加到聊天窗口function! chatgpt#JobStdoutHandler(j, d, e) call chatgpt#addContent('## Chatgpt:') call chatgpt#addContent(a:d) call chatgpt#addContent('--------------------------------------------------')endfunction
" 调用chatgpt.py进行聊天function! chatgpt#callPythonChat(content) " 构造命令 let cmd = "python3 " . g:chatgptPyScript . " --keyfile " . shellescape(g:openaiKeyFile) . " --model " .shellescape(g:chatgptModel) . " " . shellescape(a:content) " 如果当前存在会话,则添加--session选项 if g:currentSession != "" let cmd = cmd . " --session " . shellescape(chatgpt#sessionDataName(g:currentSession)) endif " 启动进程,并定义标准输出处理函数 call jobstart(cmd, {'on_stdout': function("chatgpt#JobStdoutHandler"), 'stdout_buffered': 1})endfunction
" 在聊天窗口中添加用户输入的聊天内容,并调用chatgpt#callPythonChat函数进行聊天function! chatgpt#chatInVim(content) call chatgpt#addContent('# You:') call chatgpt#addContent(split(a:content, '')) call chatgpt#addContent('--------------------------------------------------') call chatgpt#callPythonChat(a:content)endfunction
" 设置openai.key文件路径function! chatgpt#SetKeyFile(keyfile) let g:openaiKeyFile = a:keyfileendfunction
" 设置chatgpt模型function! chatgpt#SetModel(model) let g:chatgptModel = a:modelendfunction
" 获取被选中的文本function! chatgpt#getSelectedText() let save_reg = @a normal! gv"ay let text = @a let @a = save_reg return textendfunction
" 在聊天窗口中添加用户选中的文本并进行聊天function! chatgpt#chatViusalContent(content) let selected = chatgpt#getSelectedText() let new_content = substitute(a:content, '&', selected, 'g') call chatgpt#chatInVim(new_content)endfunction
" 添加一个键映射,快捷键为key,执行chatViusalContent函数function! chatgpt#AddConfig(key, content) execute 'vnoremap <silent> '. a:key . ' :<bs><bs><bs><bs><bs>call chatgpt#chatViusalContent("' . a:content . '")<cr>'endfunction
" 根据会话名称生成会话文件名function! chatgpt#sessionFileName(session) if a:session == "" return "" endif return $HOME . "/.chatgpt/" . a:session . "_chat"endfunction
" 根据会话名称生成元数据文件名function! chatgpt#sessionDataName(session) if a:session == "" return "" endif return $HOME . "/.chatgpt/" . a:session . "_meta"endfunction
" 将会话名称转换为安全的文件名function! chatgpt#safeSession(str) let pathstr = substitute(a:str, '[^[:alnum:]]', '_', 'g') let pathstr = substitute(pathstr, '__*', '_', 'g') let pathstr = substitute(pathstr, '^_', '', '') let pathstr = substitute(pathstr, '_$', '', '') let pathstr = tolower(pathstr) return pathstrendfunction
" 加载指定会话数据到缓冲区function! chatgpt#LoadSession() let session = input("Chat Session Name:") if session == "" return endif call chatgpt#wipeBuf() call system("mkdir -p ~/.chatgpt") let g:currentSession = session let sessionFile = chatgpt#sessionFileName(session) if filereadable(sessionFile) let data = readfile(sessionFile) call chatgpt#addContent(data, 0) endifendfunction
" 关闭当前会话并清空缓冲区function! chatgpt#CloseSession() let g:currentSession = "" call chatgpt#wipeBuf()endfunction
" 删除指定会话及其相关文件function! chatgpt#DeleteSession() let session = input("Chat Session Name:") if session == "" return endif let session = chatgpt#safeSession(session) if g:currentSession == session call chatgpt#wipeBuf() let g:currentSession = "" endif let sessionData = chatgpt#sessionDataName(session) let sessionFile = chatgpt#sessionFileName(session) call system("rm -f " . sessionData) call system("rm -f " . sessionFile)endfunction
" 在聊天窗口中输入聊天内容function! chatgpt#Chat() let content = input("You say:") if content == "" return endif call chatgpt#chatInVim(content)endfunction
" 在退出Vim之前清理聊天窗口augroup chatgptWipeBuf autocmd! autocmd VimLeave * :call chatgpt#wipeBuf() autocmd VimEnter * :call chatgpt#wipeBuf() autocmd SessionLoadPost * :call chatgpt#wipeBuf()augroup END

复制代码

插件使用

插件地址:https://github.com/skyfireitdiy/chatgpt


以下是插件的中文介绍(readme 翻译):


ChatGPT Vim 插件是一个在 Vim 中使用 OpenAI 的 GPT 模型提供聊天接口的插件。它允许您与人工智能进行实时聊天,并获得实时响应。

安装

要安装此插件,您可以使用自己喜欢的插件管理器。例如,如果您使用 Vim-Plug,可以在 vimrc 文件中添加以下行:


Plug 'skyfireitdiy/chatgpt'
复制代码

用法

要请求某些问题,您可以使用:call chatgpt#Chat()命令。它将提示您输入要发送到 AI 的文本,并在缓冲区中显示响应。


会话功能使 ChatGPT 具备进行连续对话的功能。因此,在提出问题之前,您可以先设置会话。


您还可以使用以下命令:


  • :call chatgpt#LoadSession():创建新会话或加载现有聊天会话。

  • :call chatgpt#DeleteSession():删除现有聊天会话。

  • :call chatgpt#CloseSession():关闭当前聊天会话。

  • :call chatgpt#SetModel(model):设置要使用的 GPT 模型。默认模型为"gpt-3.5-turbo"。

  • :call chatgpt#SetKeyFile(keyfile):设置您的 OpenAI API 密钥文件的路径。默认路径为"$HOME/.openai.key"。

  • :call chatgpt#OpenWindow():打开聊天窗口。

配置

该插件提供了一些示例键映射,可用于快速启动具有特定提示的聊天会话。要添加自己的键映射,请使用命令:call chatgpt#AddConfig(key,content)。将"key"替换为您要使用的键映射,将"content"替换为您要开始聊天会话的提示。在使用键映射时,提示可以包含一个"&"符号,该符号将在当前选择的文本(如果有)时被替换。


示例配置:


nnoremap <silent><leader>cg :call chatgpt#Chat()<cr>nnoremap <silent><leader>cL :call chatgpt#LoadSession()<cr>nnoremap <silent><leader>cD :call chatgpt#DeleteSession()<cr>nnoremap <silent><leader>cC :call chatgpt#CloseSession()<cr>nnoremap <silent><leader>cO :call chatgpt#OpenWindow()<cr>
call chatgpt#AddConfig('<leader>ce', 'Please explain the following code: &')call chatgpt#AddConfig('<leader>cd', 'Is there anything wrong with the following code: &')call chatgpt#AddConfig('<leader>cpp', 'Please implement the following functionality in C++: &')call chatgpt#AddConfig('<leader>cgo', 'Please implement the following functionality in Go: &')call chatgpt#AddConfig('<leader>cpy', 'Please implement the following functionality in Python: &')call chatgpt#AddConfig('<leader>ca', '&')call chatgpt#AddConfig('<leader>cw', 'Write an article on "&" using markdown')call chatgpt#AddConfig('<leader>c?', 'What is &?')call chatgpt#AddConfig('<leader>ch', 'How do I &?')
复制代码

许可

ChatGPT Vim 插件采用 MIT 许可证发布。有关详细信息,请参见 LICENSE 文件。

总结

本文介绍了一种基于 Neovim 的插件开发方法,使用 Python 调用 OpenAI API 来实现智能对话的功能。我们学习了如何使用 OpenAI 的 Python SDK 来调用 API 接口,以及如何使用 Vimscript 来异步执行 Shell 命令,以便在 Neovim 中运行 Python 脚本。最后,我们还介绍了如何使用 ChatGPT 来实现连续对话的功能,并给出了插件的具体实现代码和使用说明。这个插件可以帮助用户在 Neovim 中进行智能对话,提高工作效率和用户体验。希望本文能够对使用 Neovim 的开发者们有所帮助。


发布于: 刚刚阅读数: 3
用户头像

SkyFire

关注

这个cpper很懒,什么都没留下 2018-10-13 加入

会一点点cpp的苦逼码农

评论

发布
暂无评论
强强联合:Neovim+ChatGPT | 社区征文_ChatGPT_SkyFire_InfoQ写作社区