Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ localwriter.json

# Build artifacts
*.oxt
*.rdb
*.pyc
__pycache__/

Expand Down
32 changes: 32 additions & 0 deletions CalcAddIn.xcu
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<oor:component-data xmlns:oor="http://openoffice.org/2001/registry"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
oor:name="CalcAddIns" oor:package="org.openoffice.Office">
<node oor:name="AddInInfo">
<node oor:name="org.extension.localwriter.PromptFunction" oor:op="replace">
<node oor:name="AddInFunctions">
<node oor:name="prompt" oor:op="replace">
<prop oor:name="DisplayName">
<value xml:lang="en-US">PROMPT</value>
</prop>
<prop oor:name="Description">
<value xml:lang="en-US">Generates text using an LLM</value>
</prop>
<prop oor:name="Category">
<value>Add-In</value>
</prop>
<node oor:name="Parameters">
<node oor:name="message" oor:op="replace">
<prop oor:name="DisplayName">
<value xml:lang="en-US">message</value>
</prop>
<prop oor:name="Description">
<value xml:lang="en-US">The prompt to send to the LLM</value>
</prop>
</node>
</node>
</node>
</node>
</node>
</node>
</oor:component-data>
4 changes: 4 additions & 0 deletions META-INF/manifest.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE manifest:manifest PUBLIC "-//OpenOffice.org//DTD Manifest 1.0//EN" "Manifest.dtd">
<manifest:manifest xmlns:manifest="http://openoffice.org/2001/manifest">
<manifest:file-entry manifest:media-type="application/vnd.sun.star.uno-typelibrary;type=RDB"
manifest:full-path="XPromptFunction.rdb"/>
<manifest:file-entry manifest:media-type="application/vnd.sun.star.uno-component;type=Python" manifest:full-path="main.py" />
<manifest:file-entry manifest:media-type="application/vnd.sun.star.uno-component;type=Python" manifest:full-path="prompt_function.py" />
<manifest:file-entry manifest:full-path="pkg-desc/pkg-description.en" manifest:media-type="application/vnd.sun.star.package-bundle-description;locale=en"/>
<manifest:file-entry manifest:full-path="Addons.xcu" manifest:media-type="application/vnd.sun.star.configuration-data"/>
<manifest:file-entry manifest:full-path="Accelerators.xcu" manifest:media-type="application/vnd.sun.star.configuration-data"/>
<manifest:file-entry manifest:full-path="CalcAddIn.xcu" manifest:media-type="application/vnd.sun.star.configuration-data"/>
</manifest:manifest>

19 changes: 19 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
2 changes: 1 addition & 1 deletion description.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<identifier value="org.extension.sample"/>
<identifier value="org.extension.localwriter"/>
<version value="0.0.9"/>
<registration>
<simple-license accept-by="admin" default-license-id="ID0" suppress-on-update="true" >
Expand Down
13 changes: 13 additions & 0 deletions idl/XPromptFunction.idl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#ifndef __org_extension_localwriter_PromptFunction_XPromptFunction_idl__
#define __org_extension_localwriter_PromptFunction_XPromptFunction_idl__

#include <com/sun/star/uno/XInterface.idl>

module org { module extension { module localwriter { module PromptFunction {
interface XPromptFunction : com::sun::star::uno::XInterface
{
string prompt( [in] string message );
};
}; }; }; };

#endif
141 changes: 141 additions & 0 deletions prompt_function.py
Original file line number Diff line number Diff line change
@@ -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",),
)