From b31518e12e559fd61368c86a297de578e484bfc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mo=C8=99negu=C8=9Bu=20Adrian-Ioan?= Date: Sun, 20 Jul 2025 11:53:09 +0300 Subject: [PATCH 1/7] feat: add google mcp integration --- lua/docscribe/config.lua | 20 ++++-- lua/docscribe/llm/init.lua | 18 ++++-- lua/docscribe/llm/providers/google.lua | 90 ++++++++++++++++++++++++++ lua/docscribe/llm/providers/ollama.lua | 27 +++++--- 4 files changed, 135 insertions(+), 20 deletions(-) create mode 100644 lua/docscribe/llm/providers/google.lua diff --git a/lua/docscribe/config.lua b/lua/docscribe/config.lua index 39b0243..e145d75 100644 --- a/lua/docscribe/config.lua +++ b/lua/docscribe/config.lua @@ -7,16 +7,24 @@ local prompt_templates = require("docscribe.prompt_templates") local config = { ui = { highlight = { - style = "signature", -- "signature" | "full" | "none" function highlight - timeout = 2000, -- Time (ms) before highlight fades - bg = "#545454", -- Highlight background color + style = "signature", -- "signature" | "full" | "none" function highlight + timeout = 2000, -- Time (ms) before highlight fades + bg = "#545454", -- Highlight background color }, }, llm = { - provider = "ollama", -- Backend used for LLM (e.g., ollama, openai) - model = "llama3.2", -- Default model used for docs + provider = "ollama", -- Backend used for LLM (e.g., ollama, google) + provider_opts = { + ollama = { + model = "llama3.2", -- Default model used for docs + }, + google = { + model = "gemini-1.5-flash", + api_key = os.getenv("GOOGLE_API_KEY"), -- API key for Google's Gemini + }, + }, }, - prompt_templates = { -- Set of prompt templates for each programming language + prompt_templates = { -- Set of prompt templates for each programming language default = prompt_templates.default_template, h = prompt_templates.c_template, c = prompt_templates.c_template, diff --git a/lua/docscribe/llm/init.lua b/lua/docscribe/llm/init.lua index c44158f..5ed3467 100644 --- a/lua/docscribe/llm/init.lua +++ b/lua/docscribe/llm/init.lua @@ -2,7 +2,11 @@ --- Generates documentation using LLM providers. local config = require("docscribe.config") -local ollama = require("docscribe.llm.providers.ollama") + +local providers = { + ollama = require("docscribe.llm.providers.ollama"), + google = require("docscribe.llm.providers.google"), +} local M = {} @@ -16,12 +20,16 @@ function M.generate_docs(function_code, callback) local prompt_template = prompt_templates[lang] or prompt_templates.default local prompt = prompt_template:gsub("{{code}}", function_code) - local llm = config.get_config("llm") + local llm_config = config.get_config("llm") + local provider_name = llm_config.provider + local provider_opts = llm_config.provider_opts[provider_name] + + local provider = providers[provider_name] - if llm.provider == "ollama" then - ollama.generate_response(prompt, llm.model, callback) + if provider then + provider.generate_response(prompt, callback, provider_opts) else - callback(nil, 'Invalid LLM runner "' .. llm.provider .. '"') + callback(nil, 'Invalid LLM provider "' .. provider_name .. '"') end end diff --git a/lua/docscribe/llm/providers/google.lua b/lua/docscribe/llm/providers/google.lua new file mode 100644 index 0000000..4e798dc --- /dev/null +++ b/lua/docscribe/llm/providers/google.lua @@ -0,0 +1,90 @@ +--- @module "docscribe.llm.providers.google" +--- Interface to the Google LLM provider. + +local Job = require("plenary.job") + +local M = {} + +--- Handles the exit of the curl job for the Google provider. +--- @param job table The job object. +--- @param code number The exit code. +--- @param callback fun(docs: string|nil, err_msg: string|nil) The callback function. +local function handle_job_exit(job, code, callback) + if code ~= 0 then + local error_message = table.concat(job:stderr_result(), "") + callback(nil, "Error generating Google response: " .. error_message) + return + end + + local result = table.concat(job:result(), "") + local ok, data = pcall(vim.json.decode, result) + + if not ok then + callback(nil, "Failed to decode Google API response: " .. tostring(data)) + return + end + + if data.error then + callback(nil, "Google API error: " .. data.error.message) + return + end + + if + data.candidates + and data.candidates[1] + and data.candidates[1].content + and data.candidates[1].content.parts + and data.candidates[1].content.parts[1] + then + callback(data.candidates[1].content.parts[1].text) + else + callback(nil, "Could not extract text from Google API response") + end +end + +--- Generates a response using the Google LLM provider. +--- @param prompt string The prompt to send to Google. +--- @param callback fun(docs: string|nil, err_msg: string|nil) Callback with the response or an error. +--- @param opts table The options for the Google provider. Must contain `model` and `api_key`. +function M.generate_response(prompt, callback, opts) + if not opts.api_key or opts.api_key == "" then + callback(nil, "Google API key is not configured. Set the GOOGLE_API_KEY environment variable.") + return + end + + local url = "https://generativelanguage.googleapis.com/v1beta/models/" .. opts.model .. ":generateContent" + + local body = { + contents = { + { + parts = { + { + text = prompt, + }, + }, + }, + }, + } + + --- @diagnostic disable-next-line: missing-fields + Job:new({ + command = "curl", + args = { + "-X", + "POST", + "-H", + "Content-Type: application/json", + "-H", + "x-goog-api-key: " .. opts.api_key, + url, + "-d", + vim.json.encode(body), + }, + on_exit = function(job, code) + handle_job_exit(job, code, callback) + end, + }):start() +end + +return M + diff --git a/lua/docscribe/llm/providers/ollama.lua b/lua/docscribe/llm/providers/ollama.lua index 9bac473..1e287c9 100644 --- a/lua/docscribe/llm/providers/ollama.lua +++ b/lua/docscribe/llm/providers/ollama.lua @@ -5,23 +5,32 @@ local Job = require("plenary.job") local M = {} +--- Handles the exit of the Ollama job. +--- @param job table The job object. +--- @param code number The exit code. +--- @param callback fun(docs: string|nil, err_msg: string|nil) The callback function. +local function handle_job_exit(job, code, callback) + if code ~= 0 then + callback(nil, "Error generating Ollama response") + return + end + + local docs = table.concat(job:result(), "\n") + callback(docs) +end + --- Generates a response using the Ollama LLM provider. --- @param prompt string The prompt to send to Ollama. ---- @param model string The Ollama model to use. --- @param callback fun(docs: string|nil, err_msg: string|nil) Callback with the response or an error. -function M.generate_response(prompt, model, callback) +--- @param opts table The Ollama model to use. +function M.generate_response(prompt, callback, opts) --- @diagnostic disable-next-line: missing-fields Job:new({ command = "ollama", - args = { "run", model }, + args = { "run", opts.model }, writer = prompt, on_exit = function(job, code) - if code == 0 then - local docs = table.concat(job:result(), "\n") - callback(docs) - else - callback(nil, "Error generating Ollama response") - end + handle_job_exit(job, code, callback) end, }):start() end From 9c1676768259e3e6379732068d017b9e6c36ff13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mo=C8=99negu=C8=9Bu=20Adrian-Ioan?= Date: Sun, 20 Jul 2025 11:55:52 +0300 Subject: [PATCH 2/7] chore: rewrite better prompt templates --- lua/docscribe/prompt_templates/c.lua | 32 +++++++++-------- lua/docscribe/prompt_templates/cpp.lua | 34 +++++++++---------- lua/docscribe/prompt_templates/default.lua | 30 +++++++++------- lua/docscribe/prompt_templates/init.lua | 13 ++++--- lua/docscribe/prompt_templates/java.lua | 31 +++++++++-------- lua/docscribe/prompt_templates/javascript.lua | 33 ++++++++++-------- lua/docscribe/prompt_templates/lua.lua | 32 +++++++++-------- lua/docscribe/prompt_templates/python.lua | 33 ++++++++++-------- lua/docscribe/prompt_templates/typescript.lua | 33 ++++++++++-------- 9 files changed, 149 insertions(+), 122 deletions(-) diff --git a/lua/docscribe/prompt_templates/c.lua b/lua/docscribe/prompt_templates/c.lua index 4d38dce..432a589 100644 --- a/lua/docscribe/prompt_templates/c.lua +++ b/lua/docscribe/prompt_templates/c.lua @@ -1,21 +1,23 @@ +--- @module "docscribe.prompt_templates.c" + return [[ - You are a documentation assistant. +You are an expert C programmer specializing in writing documentation that follows the Doxygen standard. - Generate a Doxygen-style comment block for the C function below. The output must: +Your response must contain ONLY the Doxygen comment block. +Do NOT include the original function code in your response. +Do NOT wrap the documentation in markdown code fences (e.g., ```). - 1. Start with `/**` and end with `*/`. - 2. Begin with a `@brief` tag summarizing the function. - 3. Follow `@brief` with any additional tags, in this order: - - `@param` for each parameter (name, type, purpose). - - `@return` to describe the return value. - - `@example` for one or more usage examples. - 4. Document all function parameters using `@param`, describing their name, type, and purpose. - 5. Include a `@return` tag describing the return value. - 6. If needed, include an `@example` showing how to use the function. +Generate a Doxygen-style comment block for the following C function. - Do NOT include the function code or any Markdown formatting. +Follow these rules: +1. Use `/** ... */` for the comment block. +2. The main description should be a single, brief paragraph. +3. Use `@param` for parameters and `@return` for the return value. +4. If the function's logic is complex, add a `@note` section to explain important details or non-obvious usage. Do not use `@example`. +5. Add a blank line between the main description and the tag sections. - ```c - {{code}} - ``` +Code: +```c +{{code}} +``` ]] diff --git a/lua/docscribe/prompt_templates/cpp.lua b/lua/docscribe/prompt_templates/cpp.lua index 9b196eb..8553d2b 100644 --- a/lua/docscribe/prompt_templates/cpp.lua +++ b/lua/docscribe/prompt_templates/cpp.lua @@ -1,23 +1,23 @@ +--- @module "docscribe.prompt_templates.cpp" + return [[ - You are a documentation assistant. +You are an expert C++ programmer specializing in writing documentation that follows the Doxygen standard. - Generate a Doxygen-style comment block for the C++ function below. The output must: +Your response must contain ONLY the Doxygen comment block. +Do NOT include the original function code in your response. +Do NOT wrap the documentation in markdown code fences (e.g., ```). - 1. Start with `/**` and end with `*/`. - 2. Begin with a `@brief` tag summarizing the function. - 3. Follow `@brief` with any additional tags, in this order: - - `@param` for each parameter (name, type, purpose). - - `@return` to describe the return value. - - `@throws` for each possible exception. - - `@example` for one or more usage examples. - 4. Document all function parameters using `@param`, describing their name, type, and purpose. - 5. Include a `@return` tag describing the return value. - 6. If the function may throw exceptions, use `@throws` for each possible exception. - 7. Include an `@example` block demonstrating typical usage. +Generate a Doxygen-style comment block for the following C++ function. - Do NOT include the function code or any Markdown formatting. +Follow these rules: +1. Use `/** ... */` for the comment block. +2. The main description should be a single, brief paragraph. +3. Use `@param` for parameters and `@return` for the return value. +4. If the function's logic is complex, add a `@note` section to explain important details or non-obvious usage. Do not use `@example`. +5. Add a blank line between the main description and the tag sections. - ```cpp - {{code}} - ``` +Code: +```cpp +{{code}} +``` ]] diff --git a/lua/docscribe/prompt_templates/default.lua b/lua/docscribe/prompt_templates/default.lua index 0b44b8e..7b74a50 100644 --- a/lua/docscribe/prompt_templates/default.lua +++ b/lua/docscribe/prompt_templates/default.lua @@ -1,19 +1,23 @@ +--- @module "docscribe.prompt_templates.default" + return [[ - You are a documentation assistant. +You are an expert programmer tasked with writing clear, concise, and accurate documentation for a given function. - Generate a **proper documentation comment block** for the function below. The output must: +Your response must contain ONLY the documentation block. +Do NOT include the original function code in your response. +Do NOT wrap the documentation in markdown code fences (e.g., ```). - 1. Include a **clear and concise function description**. - 2. Document **each parameter** with the appropriate tags, describing the name, type, and purpose. - 3. Include a **return value** description with the return type and a brief explanation. - 4. If the function throws any exceptions, include an exception tag. - 5. Include an example block showing typical usage. +Generate documentation for the following code. - **Do NOT wrap the output in backticks, triple backticks, or any Markdown formatting.** - Do **not** include the function code in your output. - Just return the unwrapped documentation block as plain text. +Follow these rules: +1. The documentation should be a block comment. +2. The main description should be a single, brief paragraph explaining the function's purpose. Do not describe the parameters or return value in the description. +3. Use tags (e.g., @param, @return) to describe parameters and return values. +4. Only include a usage example if the function's logic is complex or has important edge cases. The example must be meaningful and demonstrate a non-obvious use case. Do not show a trivial call. +5. Add a blank line between the main description and the tag sections. - ``` - {{code}} - ``` +Code: +``` +{{code}} +``` ]] diff --git a/lua/docscribe/prompt_templates/init.lua b/lua/docscribe/prompt_templates/init.lua index 569b8f2..9bc5151 100644 --- a/lua/docscribe/prompt_templates/init.lua +++ b/lua/docscribe/prompt_templates/init.lua @@ -1,10 +1,15 @@ -return { - default_template = require("docscribe.prompt_templates.default"), +--- @module "docscribe.prompt_templates" +--- Central hub for all prompt templates. + +local M = { c_template = require("docscribe.prompt_templates.c"), cpp_template = require("docscribe.prompt_templates.cpp"), - python_template = require("docscribe.prompt_templates.python"), + default_template = require("docscribe.prompt_templates.default"), java_template = require("docscribe.prompt_templates.java"), - lua_template = require("docscribe.prompt_templates.lua"), javascript_template = require("docscribe.prompt_templates.javascript"), + lua_template = require("docscribe.prompt_templates.lua"), + python_template = require("docscribe.prompt_templates.python"), typescript_template = require("docscribe.prompt_templates.typescript"), } + +return M diff --git a/lua/docscribe/prompt_templates/java.lua b/lua/docscribe/prompt_templates/java.lua index c49cbb7..4c09b7b 100644 --- a/lua/docscribe/prompt_templates/java.lua +++ b/lua/docscribe/prompt_templates/java.lua @@ -1,20 +1,23 @@ +--- @module "docscribe.prompt_templates.java" + return [[ - You are a documentation assistant. +You are an expert Java programmer specializing in writing documentation that follows the Javadoc standard. - Generate a **Java Javadoc** comment block for the method below. The output must: +Your response must contain ONLY the Javadoc comment block. +Do NOT include the original function code in your response. +Do NOT wrap the documentation in markdown code fences (e.g., ```). - 1. Start with `/**` and end with `*/` (standard Javadoc format). - 2. Include a **clear and concise method description**. - 3. Document **each parameter** with `@param`, describing the name, type, and purpose. - 4. Include a `@return` tag with a description of the return value and its type. - 5. If the method throws any exceptions, include a `@throws` tag for each exception, describing the exception type and the condition under which it is thrown. - 6. Include an `@example` block showing one or two typical usages. +Generate a Javadoc-style comment block for the following Java method. - **Do NOT wrap the output in backticks, triple backticks, or any Markdown formatting.** - Do **not** include the method code in your output. - Just return the unwrapped Javadoc block as plain text. +Follow these rules: +1. Use `/** ... */` for the comment block. +2. The main description should be a single, brief paragraph. +3. Use `@param` for parameters, `@return` for the return value, and `@throws` for exceptions. +4. Only include a usage example if the method's logic is complex or has important edge cases. The example must be meaningful and demonstrate a non-obvious use case. Wrap the example in `{@code ...}`. +5. Add a blank line between the main description and the tag sections. - ```java - {{code}} - ``` +Code: +```java +{{code}} +``` ]] diff --git a/lua/docscribe/prompt_templates/javascript.lua b/lua/docscribe/prompt_templates/javascript.lua index dd02ea5..1793be4 100644 --- a/lua/docscribe/prompt_templates/javascript.lua +++ b/lua/docscribe/prompt_templates/javascript.lua @@ -1,20 +1,23 @@ +--- @module "docscribe.prompt_templates.javascript" + return [[ - You are a documentation assistant. +You are an expert JavaScript programmer specializing in writing documentation that follows the JSDoc standard. - Generate a **JavaScript JSDoc** comment block for the function below. The output must: +Your response must contain ONLY the JSDoc comment block. +Do NOT include the original function code in your response. +Do NOT wrap the documentation in markdown code fences (e.g., ```). - 1. Start with `/**` and end with `*/` (standard JSDoc format). - 2. Include a **clear and concise function description**. - 3. Document **each parameter** with `@param`, describing the name and purpose. - 4. Include a `@returns` tag with a description of the return value. - 5. If the function throws any exceptions, include a `@throws` tag for each. - 6. Include a `@example` block showing one or two typical usages. +Generate a JSDoc-style comment block for the following JavaScript function. - **Do NOT wrap the output in backticks, triple backticks, or any Markdown formatting.** - Do **not** include the function code in your output. - Just return the unwrapped JSDoc block as plain text. +Follow these rules: +1. Use `/** ... */` for the comment block. +2. The main description should be a single, brief paragraph. +3. Use `@param` for parameters, `@returns` for the return value, and include types in curly braces (e.g., `{string}`). +4. Only include a usage example if the function's logic is complex or has important edge cases. The example must be meaningful and demonstrate a non-obvious use case. Do not show a trivial call. +5. Add a blank line between the main description and the tag sections. - ```javascript - {{code}} - ``` -]] +Code: +```javascript +{{code}} +``` +]] \ No newline at end of file diff --git a/lua/docscribe/prompt_templates/lua.lua b/lua/docscribe/prompt_templates/lua.lua index a00d8ba..f714083 100644 --- a/lua/docscribe/prompt_templates/lua.lua +++ b/lua/docscribe/prompt_templates/lua.lua @@ -1,19 +1,23 @@ +--- @module "docscribe.prompt_templates.lua" + return [[ - You are a documentation assistant. +You are an expert Lua programmer specializing in writing documentation that follows the EmmyLua/LuaDoc standard. - Generate a **Lua docstring** for the function below. The output must: +Your response must contain ONLY the LuaDoc comment block. +Do NOT include the original function code in your response. +Do NOT wrap the documentation in markdown code fences (e.g., ```). - 1. Start with `---` for each line (standard Lua docstring format). - 2. Include a **clear and concise function description**. - 3. Document **each parameter** with `@param`, including the name, type, and a brief description. - 4. Include a `@return` tag for each return value, with its type and a brief description. - 5. If the function raises any errors, include a `@error` tag with a description of the error. +Generate a LuaDoc-style comment block for the following Lua function. - **Do NOT wrap the output in backticks, triple backticks, or any Markdown formatting.** - Do **not** include the function code in your output. - Just return the unwrapped docstring as plain text. +Follow these rules: +1. Use `---` for each line of the comment block. +2. The main description should be a single, brief paragraph. +3. Use `@param` for parameters and `@return` for the return value. Include types where appropriate. +4. Only include a usage example if the function's logic is complex or has important edge cases. The example must be meaningful and demonstrate a non-obvious use case. Use the `@usage` tag for the example. +5. Add a blank line between the main description and the tag sections. - ```lua - {{code}} - ``` -]] +Code: +```lua +{{code}} +``` +]] \ No newline at end of file diff --git a/lua/docscribe/prompt_templates/python.lua b/lua/docscribe/prompt_templates/python.lua index 0753948..726a8f3 100644 --- a/lua/docscribe/prompt_templates/python.lua +++ b/lua/docscribe/prompt_templates/python.lua @@ -1,20 +1,23 @@ +--- @module "docscribe.prompt_templates.python" + return [[ - You are a documentation assistant. +You are an expert Python programmer specializing in writing docstrings that follow the PEP 257 standard. - Generate a **Python docstring** for the function below. The output must: +Your response must contain ONLY the docstring. +Do NOT include the original function code in your response. +Do NOT wrap the documentation in markdown code fences (e.g., ```). - 1. Start with a `"""` and end with `"""` (standard Python docstring format). - 2. Include a **clear and concise function description**. - 3. Document **each parameter** with `:param`, describing the name, type, and purpose. - 4. Include a `:return:` tag with a description of the return value and its type. - 5. If the function raises any exceptions, include a `:raises` tag for each exception. - 6. Include an `:example:` block showing one or two typical usages. +Generate a PEP 257-style docstring for the following Python function. - **Do NOT wrap the output in backticks, triple backticks, or any Markdown formatting.** - Do **not** include the function code in your output. - Just return the unwrapped docstring as plain text. +Follow these rules: +1. Use a triple-quoted string (`"""..."""`) for the docstring. +2. The main description should be a single, brief paragraph. +3. Use "Args:" to introduce the parameters and "Returns:" for the return value. +4. Only include a usage example if the function's logic is complex or has important edge cases. The example must be meaningful and demonstrate a non-obvious use case. Do not show a trivial call. +5. Add a blank line between the main description and the "Args:" section. - ```python - {{code}} - ``` -]] +Code: +```python +{{code}} +``` +]] \ No newline at end of file diff --git a/lua/docscribe/prompt_templates/typescript.lua b/lua/docscribe/prompt_templates/typescript.lua index 91fb39f..80f7cbf 100644 --- a/lua/docscribe/prompt_templates/typescript.lua +++ b/lua/docscribe/prompt_templates/typescript.lua @@ -1,20 +1,23 @@ +--- @module "docscribe.prompt_templates.typescript" + return [[ - You are a documentation assistant. +You are an expert TypeScript programmer specializing in writing documentation that follows the JSDoc standard. - Generate a **TypeScript TSDoc** comment block for the function below. The output must: +Your response must contain ONLY the JSDoc comment block. +Do NOT include the original function code in your response. +Do NOT wrap the documentation in markdown code fences (e.g., ```). - 1. Start with `/**` and end with `*/` (pure TSDoc format). - 2. Include a **clear and concise function description**. - 3. Document **each parameter** with `@param`, describing the name, type, and purpose. - 4. Include a `@returns` tag with a description of the return value. - 5. If the function throws any exceptions, include a `@throws` tag for each. - 6. Include a `@example` block showing one or two typical usages. +Generate a JSDoc-style comment block for the following TypeScript function. - **Do NOT wrap the output in backticks, triple backticks, or any Markdown formatting.** - Do **not** include the function code in your output. - Just return the unwrapped TSDoc block as plain text. +Follow these rules: +1. Use `/** ... */` for the comment block. +2. The main description should be a single, brief paragraph. +3. Use `@param` for parameters and `@returns` for the return value. Since TypeScript is typed, you do not need to include types in the JSDoc. +4. Only include a usage example if the function's logic is complex or has important edge cases. The example must be meaningful and demonstrate a non-obvious use case. Do not show a trivial call. +5. Add a blank line between the main description and the tag sections. - ```typescript - {{code}} - ``` -]] +Code: +```typescript +{{code}} +``` +]] \ No newline at end of file From 811c568d90f02b4b60677171661fb61ef9a131e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mo=C8=99negu=C8=9Bu=20Adrian-Ioan?= Date: Sun, 20 Jul 2025 11:56:30 +0300 Subject: [PATCH 3/7] fix: solve notifications concurrency issue Sometimes the success message notification wouldnt replace the loading one --- lua/docscribe/ui/notifications.lua | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lua/docscribe/ui/notifications.lua b/lua/docscribe/ui/notifications.lua index 7d5c75c..bf31d40 100644 --- a/lua/docscribe/ui/notifications.lua +++ b/lua/docscribe/ui/notifications.lua @@ -44,10 +44,20 @@ function M.start_spinner_notification() return end - current_spinner_idx = 0 + current_spinner_idx = 1 + spinner_notification_id = + M.docscribe_notify(spinner_chars[current_spinner_idx] .. " Generating docs...", vim.log.levels.WARN, { + timeout = false, + hide_from_history = true, + }) + + -- In the unlikely event that the notification could not be created + if not spinner_notification_id then + return + end spinner_timer = vim.loop.new_timer() - spinner_timer:start(0, spinner_interval, vim.schedule_wrap(update_spinner)) + spinner_timer:start(spinner_interval, spinner_interval, vim.schedule_wrap(update_spinner)) end --- Stops the spinner notification and replaces it with a final message. From 3629d1b8e3ec92c70635e905a88172712b66f6dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mo=C8=99negu=C8=9Bu=20Adrian-Ioan?= Date: Sun, 20 Jul 2025 12:17:16 +0300 Subject: [PATCH 4/7] fix: fix llm tests to reflec the new config --- tests/mocks/config.lua | 9 ++++++++- tests/unit/llm/llm_spec.lua | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/mocks/config.lua b/tests/mocks/config.lua index 255306d..a2fa7a3 100644 --- a/tests/mocks/config.lua +++ b/tests/mocks/config.lua @@ -6,7 +6,14 @@ return { default = "Default template: {{code}}", } elseif key == "llm" then - return { provider = "ollama", model = "mock_model" } + return { + provider = "ollama", + provider_opts = { + ollama = { + model = "mock_model", + }, + }, + } end end, } diff --git a/tests/unit/llm/llm_spec.lua b/tests/unit/llm/llm_spec.lua index f37f2ab..8b63e88 100644 --- a/tests/unit/llm/llm_spec.lua +++ b/tests/unit/llm/llm_spec.lua @@ -26,7 +26,7 @@ describe("docscribe.llm", function() it("handles invalid LLM provider", function() mock_config.get_config = function(key) if key == "llm" then - return { provider = "invalid_provider" } + return { provider = "invalid_provider", provider_opts = {} } elseif key == "prompt_templates" then return { lua = "Here is a Lua template: {{code}}", @@ -39,7 +39,7 @@ describe("docscribe.llm", function() M.generate_docs(code_snippet, function(docs, err_msg) assert.is_nil(docs) assert.is_not_nil(err_msg) - assert.is_equal(err_msg, 'Invalid LLM runner "invalid_provider"') + assert.is_equal(err_msg, 'Invalid LLM provider "invalid_provider"') end) end) end) From 24d1aa5b5f34b070daed8e37afa6bcdc05d927aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mo=C8=99negu=C8=9Bu=20Adrian-Ioan?= Date: Sun, 20 Jul 2025 16:00:12 +0300 Subject: [PATCH 5/7] feat: add groq support --- lua/docscribe/config.lua | 6 ++- lua/docscribe/llm/init.lua | 1 + lua/docscribe/llm/providers/groq.lua | 81 ++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 lua/docscribe/llm/providers/groq.lua diff --git a/lua/docscribe/config.lua b/lua/docscribe/config.lua index e145d75..a3163d3 100644 --- a/lua/docscribe/config.lua +++ b/lua/docscribe/config.lua @@ -13,7 +13,7 @@ local config = { }, }, llm = { - provider = "ollama", -- Backend used for LLM (e.g., ollama, google) + provider = "ollama", -- Backend used for LLM (e.g., ollama, google, groq) provider_opts = { ollama = { model = "llama3.2", -- Default model used for docs @@ -22,6 +22,10 @@ local config = { model = "gemini-1.5-flash", api_key = os.getenv("GOOGLE_API_KEY"), -- API key for Google's Gemini }, + groq = { + model = "llama-3.1-8b-instant", -- Default model used for docs + api_key = os.getenv("GROQ_API_KEY"), -- API key for Groq + }, }, }, prompt_templates = { -- Set of prompt templates for each programming language diff --git a/lua/docscribe/llm/init.lua b/lua/docscribe/llm/init.lua index 5ed3467..51a7b84 100644 --- a/lua/docscribe/llm/init.lua +++ b/lua/docscribe/llm/init.lua @@ -6,6 +6,7 @@ local config = require("docscribe.config") local providers = { ollama = require("docscribe.llm.providers.ollama"), google = require("docscribe.llm.providers.google"), + groq = require("docscribe.llm.providers.groq"), } local M = {} diff --git a/lua/docscribe/llm/providers/groq.lua b/lua/docscribe/llm/providers/groq.lua new file mode 100644 index 0000000..d745da5 --- /dev/null +++ b/lua/docscribe/llm/providers/groq.lua @@ -0,0 +1,81 @@ +--- @module "docscribe.llm.providers.groq" +--- Interface to the Groq LLM provider. + +local Job = require("plenary.job") + +local M = {} + +--- Handles the exit of the curl job for the Groq provider. +--- @param job table The job object. +--- @param code number The exit code. +--- @param callback fun(docs: string|nil, err_msg: string|nil) The callback function. +local function handle_job_exit(job, code, callback) + if code ~= 0 then + local error_message = table.concat(job:stderr_result(), "") + callback(nil, "Error generating Groq response: " .. error_message) + return + end + + local result = table.concat(job:result(), "") + local ok, data = pcall(vim.json.decode, result) + + if not ok then + callback(nil, "Failed to decode Groq API response: " .. tostring(data)) + return + end + + if data.error then + callback(nil, "Groq API error: " .. data.error.message) + return + end + + if data.choices and data.choices[1] and data.choices[1].message and data.choices[1].message.content then + callback(data.choices[1].message.content) + else + callback(nil, "Could not extract text from Groq API response") + end +end + +--- Generates a response using the Groq LLM provider. +--- @param prompt string The prompt to send to Groq. +--- @param callback fun(docs: string|nil, err_msg: string|nil) Callback with the response or an error. +--- @param opts table The options for the Groq provider. Must contain `model` and `api_key`. +function M.generate_response(prompt, callback, opts) + if not opts.api_key or opts.api_key == "" then + callback(nil, "Groq API key is not configured. Set the GROQ_API_KEY environment variable.") + return + end + + local url = "https://api.groq.com/openai/v1/chat/completions" + + local body = { + model = opts.model, + messages = { + { + role = "user", + content = prompt, + }, + }, + } + + Job:new({ + command = "curl", + args = { + "-X", + "POST", + "-H", + "Content-Type: application/json", + "-H", + "Authorization: Bearer " .. opts.api_key, + url, + "-d", + vim.json.encode(body), + }, + on_exit = function(job, code) + handle_job_exit(job, code, callback) + end, + }):start() +end + +return M + From 051587448583ffb69c04e5989d0e5bcdef9903ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mo=C8=99negu=C8=9Bu=20Adrian-Ioan?= Date: Sun, 20 Jul 2025 16:00:39 +0300 Subject: [PATCH 6/7] test: write automated tests for groq and google providers --- tests/mocks/config.lua | 8 +++++ tests/mocks/plenary-job.lua | 63 +++++++++++++++++++++++++++++++++ tests/unit/llm/google_spec.lua | 64 ++++++++++++++++++++++++++++++++++ tests/unit/llm/groq_spec.lua | 64 ++++++++++++++++++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 tests/unit/llm/google_spec.lua create mode 100644 tests/unit/llm/groq_spec.lua diff --git a/tests/mocks/config.lua b/tests/mocks/config.lua index a2fa7a3..681c8c1 100644 --- a/tests/mocks/config.lua +++ b/tests/mocks/config.lua @@ -12,6 +12,14 @@ return { ollama = { model = "mock_model", }, + google = { + model = "mock_model", + api_key = "mock_api_key", + }, + groq = { + model = "mock_model", + api_key = "mock_api_key", + }, }, } end diff --git a/tests/mocks/plenary-job.lua b/tests/mocks/plenary-job.lua index 6cfc879..2a9a0ac 100644 --- a/tests/mocks/plenary-job.lua +++ b/tests/mocks/plenary-job.lua @@ -25,6 +25,69 @@ function Job:start() end self.opts.on_exit(self, 0) -- Zero exit code for success end + elseif self.opts.command == "curl" then + -- Handle Google and Groq API calls + local is_groq = false + local is_google = false + + for _, arg in ipairs(self.opts.args) do + if arg:find("api.groq.com") then + is_groq = true + break + elseif arg:find("generativelanguage.googleapis.com") then + is_google = true + break + end + end + + if is_groq then + -- Mock Groq API response + local groq_response = { + id = "chatcmpl-mock", + object = "chat.completion", + created = 1234567890, + model = "llama-3.1-8b-instant", + choices = { + { + index = 0, + message = { + role = "assistant", + content = "Mocked documentation response" + }, + finish_reason = "stop" + } + } + } + self.result = function() + return { vim.json.encode(groq_response) } + end + self.opts.on_exit(self, 0) + elseif is_google then + -- Mock Google API response + local google_response = { + candidates = { + { + content = { + parts = { + { + text = "Mocked documentation response" + } + } + } + } + } + } + self.result = function() + return { vim.json.encode(google_response) } + end + self.opts.on_exit(self, 0) + else + -- Unknown curl request + self.result = function() + return {} + end + self.opts.on_exit(self, 1) + end else self.result = function() return {} diff --git a/tests/unit/llm/google_spec.lua b/tests/unit/llm/google_spec.lua new file mode 100644 index 0000000..9e8f2c8 --- /dev/null +++ b/tests/unit/llm/google_spec.lua @@ -0,0 +1,64 @@ +require("plenary.busted") + +local mock_config = require("tests.mocks.config") + +local M = require("docscribe.llm") + +describe("docscribe.llm google provider", function() + it("generates docs successfully with google provider", function() + mock_config.get_config = function(key) + if key == "llm" then + return { + provider = "google", + provider_opts = { + google = { + model = "gemini-1.5-flash", + api_key = "mock_api_key", + }, + }, + } + elseif key == "prompt_templates" then + return { + lua = "Here is a Lua template: {{code}}", + default = "Default template: {{code}}", + } + end + end + + local code_snippet = "function add(a, b) return a + b end" + M.generate_docs(code_snippet, function(docs, err_msg) + assert.is_nil(err_msg) + assert.is_not_nil(docs) + assert.are.same(docs, "Mocked documentation response") + end) + end) + + it("handles google provider without api key", function() + mock_config.get_config = function(key) + if key == "llm" then + return { + provider = "google", + provider_opts = { + google = { + model = "gemini-1.5-flash", + api_key = "", + }, + }, + } + elseif key == "prompt_templates" then + return { + lua = "Here is a Lua template: {{code}}", + default = "Default template: {{code}}", + } + end + end + + local code_snippet = "function add(a, b) return a + b end" + M.generate_docs(code_snippet, function(docs, err_msg) + assert.is_nil(docs) + assert.is_not_nil(err_msg) + assert.is_equal(err_msg, "Google API key is not configured. Set the GOOGLE_API_KEY environment variable.") + end) + end) +end) + diff --git a/tests/unit/llm/groq_spec.lua b/tests/unit/llm/groq_spec.lua new file mode 100644 index 0000000..a23babb --- /dev/null +++ b/tests/unit/llm/groq_spec.lua @@ -0,0 +1,64 @@ +require("plenary.busted") + +local mock_config = require("tests.mocks.config") + +local M = require("docscribe.llm") + +describe("docscribe.llm groq provider", function() + it("generates docs successfully with groq provider", function() + mock_config.get_config = function(key) + if key == "llm" then + return { + provider = "groq", + provider_opts = { + groq = { + model = "llama-3.1-8b-instant", + api_key = "mock_api_key", + }, + }, + } + elseif key == "prompt_templates" then + return { + lua = "Here is a Lua template: {{code}}", + default = "Default template: {{code}}", + } + end + end + + local code_snippet = "function add(a, b) return a + b end" + M.generate_docs(code_snippet, function(docs, err_msg) + assert.is_nil(err_msg) + assert.is_not_nil(docs) + assert.are.same(docs, "Mocked documentation response") + end) + end) + + it("handles groq provider without api key", function() + mock_config.get_config = function(key) + if key == "llm" then + return { + provider = "groq", + provider_opts = { + groq = { + model = "llama-3.1-8b-instant", + api_key = "", + }, + }, + } + elseif key == "prompt_templates" then + return { + lua = "Here is a Lua template: {{code}}", + default = "Default template: {{code}}", + } + end + end + + local code_snippet = "function add(a, b) return a + b end" + M.generate_docs(code_snippet, function(docs, err_msg) + assert.is_nil(docs) + assert.is_not_nil(err_msg) + assert.is_equal(err_msg, "Groq API key is not configured. Set the GROQ_API_KEY environment variable.") + end) + end) +end) + From 296a43ae19881dd21614e4411be99239269996d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mo=C8=99negu=C8=9Bu=20Adrian-Ioan?= Date: Sun, 20 Jul 2025 16:01:03 +0300 Subject: [PATCH 7/7] refactor: remove redundant editor warning commands --- lua/docscribe/llm/providers/google.lua | 2 -- lua/docscribe/llm/providers/ollama.lua | 1 - 2 files changed, 3 deletions(-) diff --git a/lua/docscribe/llm/providers/google.lua b/lua/docscribe/llm/providers/google.lua index 4e798dc..559ec2e 100644 --- a/lua/docscribe/llm/providers/google.lua +++ b/lua/docscribe/llm/providers/google.lua @@ -66,7 +66,6 @@ function M.generate_response(prompt, callback, opts) }, } - --- @diagnostic disable-next-line: missing-fields Job:new({ command = "curl", args = { @@ -87,4 +86,3 @@ function M.generate_response(prompt, callback, opts) end return M - diff --git a/lua/docscribe/llm/providers/ollama.lua b/lua/docscribe/llm/providers/ollama.lua index 1e287c9..4862d13 100644 --- a/lua/docscribe/llm/providers/ollama.lua +++ b/lua/docscribe/llm/providers/ollama.lua @@ -24,7 +24,6 @@ end --- @param callback fun(docs: string|nil, err_msg: string|nil) Callback with the response or an error. --- @param opts table The Ollama model to use. function M.generate_response(prompt, callback, opts) - --- @diagnostic disable-next-line: missing-fields Job:new({ command = "ollama", args = { "run", opts.model },