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",),
+)