目录
Neovim 是一款备受开发者喜爱的文本编辑器,而 ChatGPT 则是 OpenAI 所推出的强大语言模型。这两者的结合可以带来令人惊叹的功能和效率,成为程序员们必备的工具之一。在本文中,我们将探讨 Neovim 和 ChatGPT 的强强联合,以及如何利用它们来提高编码和写作的体验。
要编写一个 ChatGPT 的 Neovim 插件,我的思路如下:
使用 python 调用 ChatGPT 的接口
在 vimscript 中异步调用 python,获取返回信息
在 vimscript 和 python 中 session 提供对连续对话的支持
ChatGPT 的 python 接口
接口文档参考:https://platform.openai.com/docs/guides/chat
示例代码如下:
import os
import openai
openai.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 代码导入了 os
和 openai
模块,使用 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)
endif
endfunction}, 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 work
import 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
字段中回传服务器就可以实现连续对话了。
所以我们需要将每次调用的请求信息与响应信息存储一下,在下一次请求的时候带上。
插件开发
插件分为两个部分:
python 文件,提供对 ChatGPT 的访问
vimscript 文件,提供对 python 脚本的调用以及结果展示
下面我使用 ChatGPT 对具体的实现增加了中文注释。
python 部分
# 导入必要的模块
import os
import argparse
import json
# 尝试导入 openai
try:
import openai
except 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 key
def 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 new
endfunction
" 在聊天窗口中添加聊天内容
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! G
endfunction
" 清除聊天窗口
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:keyfile
endfunction
" 设置chatgpt模型
function! chatgpt#SetModel(model)
let g:chatgptModel = a:model
endfunction
" 获取被选中的文本
function! chatgpt#getSelectedText()
let save_reg = @a
normal! gv"ay
let text = @a
let @a = save_reg
return text
endfunction
" 在聊天窗口中添加用户选中的文本并进行聊天
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 pathstr
endfunction
" 加载指定会话数据到缓冲区
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)
endif
endfunction
" 关闭当前会话并清空缓冲区
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 的开发者们有所帮助。
评论