diff --git a/.gitignore b/.gitignore index 6002860a..1f95e45d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ localwriter.json # Build artifacts *.oxt +*.rdb *.pyc __pycache__/ diff --git a/CalcAddIn.xcu b/CalcAddIn.xcu new file mode 100644 index 00000000..fd44f268 --- /dev/null +++ b/CalcAddIn.xcu @@ -0,0 +1,32 @@ + + + + + + + + PROMPT + + + Generates text using an LLM + + + Add-In + + + + + message + + + The prompt to send to the LLM + + + + + + + + diff --git a/META-INF/manifest.xml b/META-INF/manifest.xml index 2be33caa..6b7ee4df 100644 --- a/META-INF/manifest.xml +++ b/META-INF/manifest.xml @@ -1,9 +1,13 @@ + + + diff --git a/build.sh b/build.sh index 0aacf028..9c6f7a39 100755 --- a/build.sh +++ b/build.sh @@ -10,14 +10,33 @@ if [ -f "${EXTENSION_NAME}.oxt" ]; then rm "${EXTENSION_NAME}.oxt" fi +# Build UNO type library from IDL if idl/ directory exists +if [ -d "idl" ] && [ -f "idl/XPromptFunction.idl" ]; then + echo "Building XPromptFunction.rdb from IDL..." + TYPES_RDB=$(find /usr/lib/libreoffice /usr/share/libreoffice -name "types.rdb" 2>/dev/null | head -1) + OFFAPI_RDB=$(find /usr/lib/libreoffice /usr/share/libreoffice -path "*/types/offapi.rdb" 2>/dev/null | head -1) + UNOIDL_WRITE=$(which unoidl-write 2>/dev/null || find /usr/lib/libreoffice /usr/share/libreoffice -name "unoidl-write" 2>/dev/null | head -1) + + if [ -n "$UNOIDL_WRITE" ] && [ -n "$TYPES_RDB" ] && [ -n "$OFFAPI_RDB" ]; then + "$UNOIDL_WRITE" "$TYPES_RDB" "$OFFAPI_RDB" idl/XPromptFunction.idl XPromptFunction.rdb + echo "XPromptFunction.rdb built successfully" + else + echo "Warning: unoidl-write or LibreOffice type libraries not found, skipping RDB build" + echo " Install libreoffice-dev to enable: sudo apt install libreoffice-dev" + fi +fi + # Create the new package echo "Creating package ${EXTENSION_NAME}.oxt..." zip -r "${EXTENSION_NAME}.oxt" \ Accelerators.xcu \ Addons.xcu \ + CalcAddIn.xcu \ + XPromptFunction.rdb \ assets \ description.xml \ main.py \ + prompt_function.py \ pythonpath \ META-INF \ registration \ diff --git a/description.xml b/description.xml index d28eadc0..de7ee853 100644 --- a/description.xml +++ b/description.xml @@ -3,7 +3,7 @@ xmlns="http://openoffice.org/extensions/description/2006" xmlns:dep="http://openoffice.org/extensions/description/2006" xmlns:xlink="http://www.w3.org/1999/xlink"> - + diff --git a/idl/XPromptFunction.idl b/idl/XPromptFunction.idl new file mode 100644 index 00000000..4d426561 --- /dev/null +++ b/idl/XPromptFunction.idl @@ -0,0 +1,13 @@ +#ifndef __org_extension_localwriter_PromptFunction_XPromptFunction_idl__ +#define __org_extension_localwriter_PromptFunction_XPromptFunction_idl__ + +#include + +module org { module extension { module localwriter { module PromptFunction { + interface XPromptFunction : com::sun::star::uno::XInterface + { + string prompt( [in] string message ); + }; +}; }; }; }; + +#endif diff --git a/prompt_function.py b/prompt_function.py new file mode 100644 index 00000000..5521aed9 --- /dev/null +++ b/prompt_function.py @@ -0,0 +1,141 @@ +import uno +import unohelper +import json +import urllib.request +import os + +from org.extension.localwriter.PromptFunction import XPromptFunction +from llm import build_api_request, make_ssl_context + + +class PromptFunction(unohelper.Base, XPromptFunction): + def __init__(self, ctx): + self.ctx = ctx + + def getProgrammaticFunctionName(self, aDisplayName): + if aDisplayName == "PROMPT": + return "prompt" + return "" + + def getDisplayFunctionName(self, aProgrammaticName): + if aProgrammaticName == "prompt": + return "PROMPT" + return "" + + def getFunctionDescription(self, aProgrammaticName): + if aProgrammaticName == "prompt": + return "Generates text using an LLM." + return "" + + def getArgumentDescription(self, aProgrammaticName, nArgument): + if aProgrammaticName == "prompt": + if nArgument == 0: + return "The prompt to send to the LLM." + return "" + + def getArgumentName(self, aProgrammaticName, nArgument): + if aProgrammaticName == "prompt": + if nArgument == 0: + return "message" + return "" + + def hasFunctionWizard(self, aProgrammaticName): + return True + + def getArgumentCount(self, aProgrammaticName): + if aProgrammaticName == "prompt": + return 1 + return 0 + + def getArgumentIsOptional(self, aProgrammaticName, nArgument): + return False + + def getProgrammaticCategoryName(self, aProgrammaticName): + return "Add-In" + + def getDisplayCategoryName(self, aProgrammaticName): + return "Add-In" + + def getLocale(self): + return uno.createUnoStruct("com.sun.star.lang.Locale", "en", "US", "") + + def setLocale(self, locale): + pass + + def load(self, xSomething): + pass + + def unload(self): + pass + + def prompt(self, message): + try: + endpoint = str(self.get_config("endpoint", "http://localhost:11434")) + api_key = str(self.get_config("api_key", "")) + api_type = str(self.get_config("api_type", "completions")).lower() + model = str(self.get_config("model", "")) + is_owui = self.get_config("is_openwebui", False) + openai_compat = self.get_config("openai_compatibility", False) + system_prompt = str(self.get_config("extend_selection_system_prompt", "")) + max_tokens = self.get_config("extend_selection_max_tokens", 70) + + request = build_api_request( + message, endpoint, api_key, api_type, model, + is_owui, openai_compat, system_prompt, int(max_tokens)) + + # Override stream to False — Calc needs the full response at once + body = json.loads(request.data.decode('utf-8')) + body['stream'] = False + request.data = json.dumps(body).encode('utf-8') + + disable_ssl = self.get_config("disable_ssl_verification", False) + ssl_ctx = make_ssl_context(disable_ssl) + + with urllib.request.urlopen(request, context=ssl_ctx) as response: + response_json = json.loads(response.read().decode('utf-8')) + if api_type == "chat": + return response_json["choices"][0]["message"]["content"] + else: + return response_json["choices"][0]["text"] + + except urllib.error.HTTPError as e: + error_body = e.read().decode('utf-8') + return f"HTTP Error {e.code}: {error_body}" + except urllib.error.URLError as e: + return f"Connection error: {e.reason}" + except Exception as e: + return f"Error: {e}" + + def get_config(self, key, default): + name_file = "localwriter.json" + path_settings = self.ctx.getServiceManager().createInstanceWithContext( + 'com.sun.star.util.PathSettings', self.ctx) + user_config_path = getattr(path_settings, "UserConfig") + if user_config_path.startswith('file://'): + user_config_path = str(uno.fileUrlToSystemPath(user_config_path)) + config_file_path = os.path.join(user_config_path, name_file) + if not os.path.exists(config_file_path): + return default + try: + with open(config_file_path, 'r') as file: + config_data = json.load(file) + except (IOError, json.JSONDecodeError): + return default + return config_data.get(key, default) + + def getImplementationName(self): + return "org.extension.localwriter.PromptFunction" + + def supportsService(self, name): + return name in self.getSupportedServiceNames() + + def getSupportedServiceNames(self): + return ("com.sun.star.sheet.AddIn",) + + +g_ImplementationHelper = unohelper.ImplementationHelper() +g_ImplementationHelper.addImplementation( + PromptFunction, + "org.extension.localwriter.PromptFunction", + ("com.sun.star.sheet.AddIn",), +)