diff --git a/lua/gemini/api.lua b/lua/gemini/api.lua index bf3a55f..20f4f8d 100644 --- a/lua/gemini/api.lua +++ b/lua/gemini/api.lua @@ -10,91 +10,154 @@ local function get_api_key() return vim.g.gemini_api_key or os.getenv("GEMINI_API_KEY") end --- Async HTTP request function -local function async_request(url, payload, callback) - local response = "" - local error_msg = "" +-- Create a Request class to handle HTTP requests +local Request = {} +Request.__index = Request - -- Create pipes for stdout and stderr - local stdout = vim.loop.new_pipe() - local stderr = vim.loop.new_pipe() - - -- Spawn curl process with handle in broader scope - local handle - local function cleanup() - if stdout then stdout:close() end - if stderr then stderr:close() end - if handle then handle:close() end - end +function Request.new(url, payload) + local self = setmetatable({}, Request) + self.url = url + self.payload = payload + self.response = "" + self.error_msg = "" + self.stdout = vim.loop.new_pipe() + self.stderr = vim.loop.new_pipe() + self.handle = nil + return self +end - handle = vim.loop.spawn('curl', { +function Request:cleanup() + if self.stdout then self.stdout:close() end + if self.stderr then self.stderr:close() end + if self.handle then self.handle:close() end +end + +function Request:handle_error(callback, msg) + self:cleanup() + vim.schedule(function() + callback(nil, msg) + end) +end + +function Request:setup_pipes(callback) + -- Handle stdout data + self.stdout:read_start(function(err, chunk) + if err then + self:handle_error(callback, "Read error: " .. err) + return + end + if chunk then + self.response = self.response .. chunk + end + end) + + -- Handle stderr data + self.stderr:read_start(function(err, chunk) + if err then + self:handle_error(callback, "Error reading stderr: " .. err) + return + end + if chunk then + self.error_msg = self.error_msg .. chunk + end + end) +end + +function Request:execute(callback) + self.handle = vim.loop.spawn('curl', { args = { '-s', '-X', 'POST', '-H', 'Content-Type: application/json', - '-d', payload, - url + '-d', self.payload, + self.url }, - stdio = {nil, stdout, stderr} - }, function(exit_code, signal) -- This is the exit callback - -- Clean up handles - cleanup() + stdio = {nil, self.stdout, self.stderr} + }, function(exit_code) + self:cleanup() if exit_code ~= 0 then vim.schedule(function() - callback(nil, "Curl failed with code " .. exit_code .. ": " .. error_msg) + callback(nil, "Curl failed with code " .. exit_code .. ": " .. self.error_msg) end) return end -- Try to parse the response - local success, decoded = pcall(vim.json.decode, response) + local success, decoded = pcall(vim.json.decode, self.response) vim.schedule(function() if success then callback(decoded) else - callback(nil, "JSON decode error: " .. response) + callback(nil, "JSON decode error: " .. self.response) end end) end) - if not handle then - cleanup() - vim.schedule(function() - callback(nil, "Failed to start curl") - end) + if not self.handle then + self:handle_error(callback, "Failed to start curl") return end - -- Handle stdout data - stdout:read_start(function(err, chunk) - if err then - cleanup() - vim.schedule(function() - callback(nil, "Read error: " .. err) - end) - return - end - if chunk then - response = response .. chunk - end - end) - - -- Handle stderr data - stderr:read_start(function(err, chunk) - if err then - cleanup() - vim.schedule(function() - callback(nil, "Error reading stderr: " .. err) - end) - return - end - if chunk then - error_msg = error_msg .. chunk - end - end) + self:setup_pipes(callback) end +-- Message handling functions +local Message = {} + +function Message.create_contents(prompt, context) + local contents = {} + + -- Add conversation history + for _, message in ipairs(conversation_history) do + table.insert(contents, { + role = message.role, + parts = {{text = message.content}} + }) + end + + -- Add current prompt + if context then + table.insert(contents, { + role = "user", + parts = {{text = "Context:\n" .. context .. "\n\nQuery: " .. prompt}} + }) + else + table.insert(contents, { + role = "user", + parts = {{text = prompt}} + }) + end + + return contents +end + +function Message.store_message(role, content) + table.insert(conversation_history, {role = role, content = content}) +end + +function Message.handle_response(result, callback) + if result.error then + callback(nil, "API Error: " .. vim.inspect(result.error)) + return + end + + if result.candidates and + result.candidates[1] and + result.candidates[1].content and + result.candidates[1].content.parts and + result.candidates[1].content.parts[1] and + result.candidates[1].content.parts[1].text then + + local response_text = result.candidates[1].content.parts[1].text + Message.store_message("model", response_text) + callback(response_text) + else + callback(nil, "Unexpected response structure") + end +end + +-- Main API functions function M.get_response(prompt, context, callback) local api_key = get_api_key() @@ -105,86 +168,27 @@ function M.get_response(prompt, context, callback) return end - local model = "gemini-2.0-flash" - local contents = {} - - -- Add conversation history - for _, message in ipairs(conversation_history) do - table.insert(contents, { - role = message.role, - parts = {{ - text = message.content - }} - }) - end - - -- Add current prompt - if context then - table.insert(contents, { - role = "user", - parts = {{ - text = "Context:\n" .. context .. "\n\nQuery: " .. prompt - }} - }) - else - table.insert(contents, { - role = "user", - parts = {{ - text = prompt - }} - }) - end - -- Store prompt in history - table.insert(conversation_history, { - role = "user", - content = prompt - }) - - local payload = vim.json.encode({ - contents = contents, - }) + Message.store_message("user", prompt) + local contents = Message.create_contents(prompt, context) + local payload = vim.json.encode({contents = contents}) local url = string.format( "https://generativelanguage.googleapis.com/v1/models/%s:generateContent?key=%s", - model, + "gemini-2.0-flash", api_key ) - async_request(url, payload, function(result, error) + local request = Request.new(url, payload) + request:execute(function(result, error) if error then callback(nil, error) return end - - if result.error then - callback(nil, "API Error: " .. vim.inspect(result.error)) - return - end - - if result.candidates and - result.candidates[1] and - result.candidates[1].content and - result.candidates[1].content.parts and - result.candidates[1].content.parts[1] and - result.candidates[1].content.parts[1].text then - - local response_text = result.candidates[1].content.parts[1].text - - -- Store response in history - table.insert(conversation_history, { - role = "model", - content = response_text - }) - - callback(response_text) - else - callback(nil, "Unexpected response structure") - end + Message.handle_response(result, callback) end) end --- Add function to clear conversation history function M.clear_conversation() conversation_history = {} end