From e2fbe3962b5fa6279ebc911330a909e290e5f93b Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 5 Feb 2026 11:52:05 +0100 Subject: [PATCH 1/7] fix: Some prompt engineering to improve tool use Signed-off-by: Marcel Klehr --- ex_app/lib/all_tools/audio2text.py | 2 +- ex_app/lib/all_tools/contacts.py | 2 +- ex_app/lib/all_tools/context_chat.py | 2 +- ex_app/lib/all_tools/deck.py | 26 +++++++++----------------- ex_app/lib/all_tools/doc-gen.py | 4 ++-- ex_app/lib/all_tools/files.py | 26 +++++++++++++++++++++++++- ex_app/lib/all_tools/image_gen.py | 3 +-- ex_app/lib/all_tools/mail.py | 6 +++--- ex_app/lib/all_tools/talk.py | 8 ++++---- 9 files changed, 47 insertions(+), 32 deletions(-) diff --git a/ex_app/lib/all_tools/audio2text.py b/ex_app/lib/all_tools/audio2text.py index cbfffa9..395d747 100644 --- a/ex_app/lib/all_tools/audio2text.py +++ b/ex_app/lib/all_tools/audio2text.py @@ -14,7 +14,7 @@ async def get_tools(nc: Nextcloud): @safe_tool def transcribe_file(file_url: str) -> str: """ - Transcribe a media file stored inside the nextcloud + Transcribe a media file stored inside Nextcloud :param file_url: The file URL to the media file in nextcloud (The user can input this using the smart picker for example) :return: the transcription result """ diff --git a/ex_app/lib/all_tools/contacts.py b/ex_app/lib/all_tools/contacts.py index f7c08ff..0ea4d6a 100644 --- a/ex_app/lib/all_tools/contacts.py +++ b/ex_app/lib/all_tools/contacts.py @@ -80,7 +80,7 @@ def find_person_in_contacts(name: str) -> list[dict[str, typing.Any]]: @safe_tool def find_details_of_current_user() -> dict[str, typing.Any]: """ - Find the user's personal information + Find the current user's personal information :return: a dictionary with the person's personal information """ diff --git a/ex_app/lib/all_tools/context_chat.py b/ex_app/lib/all_tools/context_chat.py index 837eccf..91aa996 100644 --- a/ex_app/lib/all_tools/context_chat.py +++ b/ex_app/lib/all_tools/context_chat.py @@ -13,7 +13,7 @@ async def get_tools(nc: Nextcloud): @safe_tool def ask_context_chat(question: str) -> str: """ - Ask the context chat oracle, which knows all of the user's documents, a question about them + Ask the context chat oracle a question about the user's documents. It know the contents of all of the users documents. :param question: The question to ask :return: the answer from context chat """ diff --git a/ex_app/lib/all_tools/deck.py b/ex_app/lib/all_tools/deck.py index cba72bf..8fd5545 100644 --- a/ex_app/lib/all_tools/deck.py +++ b/ex_app/lib/all_tools/deck.py @@ -1,27 +1,19 @@ # SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors # SPDX-License-Identifier: AGPL-3.0-or-later -from datetime import datetime, timezone, timedelta -from time import sleep -from typing import Optional - -import pytz from langchain_core.tools import tool from nc_py_api import Nextcloud from nc_py_api.ex_app import LogLvl -import xml.etree.ElementTree as ET -import vobject from ex_app.lib.all_tools.lib.decorator import safe_tool, dangerous_tool -from ex_app.lib.logger import log async def get_tools(nc: Nextcloud): @tool @safe_tool - def list_decks(): + def list_boards(): """ - List all existing kanban decks with their available info + List all existing kanban boards available in the Nextcloud Deck app to the current user with their available info :return: a dictionary with all decks of the user """ @@ -35,17 +27,17 @@ def list_decks(): @tool @dangerous_tool - def add_card(deck_id: int, stack_id: int, title: str): + def add_card(board_id: int, stack_id: int, title: str): """ - Create a new card in a list of a kanban deck. - When using this tool, you need to specify in which deck and map the card should be created. - :param deck_id: the id of the deck the card should be created in, obtainable with list_decks - :param stack_id: the id of the stack the card should be created in, obtainable with list_decks + Create a new card in a list of a kanban board in the Nextcloud Deck app. + When using this tool, you need to specify in which board and map the card should be created. + :param board_id: the id of the board the card should be created in, obtainable with list_boards + :param stack_id: the id of the stack the card should be created in, obtainable with list_boards :param title: The title of the card :return: bool """ - response = nc._session._create_adapter(True).request('POST', f"{nc.app_cfg.endpoint}/index.php/apps/deck/api/v1.0//boards/{deck_id}/stacks/{stack_id}/cards", headers={ + response = nc._session._create_adapter(True).request('POST', f"{nc.app_cfg.endpoint}/index.php/apps/deck/api/v1.0/boards/{board_id}/stacks/{stack_id}/cards", headers={ "Content-Type": "application/json", }, json={ 'title': title, @@ -58,7 +50,7 @@ def add_card(deck_id: int, stack_id: int, title: str): return True return [ - list_decks, + list_boards, add_card ] diff --git a/ex_app/lib/all_tools/doc-gen.py b/ex_app/lib/all_tools/doc-gen.py index c9d41e7..72d3210 100644 --- a/ex_app/lib/all_tools/doc-gen.py +++ b/ex_app/lib/all_tools/doc-gen.py @@ -13,8 +13,8 @@ async def get_tools(nc: Nextcloud): @safe_tool def generate_document(input: str, format: str) -> str: """ - Generate a document with the input string as description. - :param text: the instructions for the document + Generate an office document based on a description of what it should contain + :param input: the instructions for what the document should contain :param format: the format of the generated file, allowed values are "text document", "pdf", "spreadsheet", "excel spreadsheet" and "slides" :return: a download link to the generated document """ diff --git a/ex_app/lib/all_tools/files.py b/ex_app/lib/all_tools/files.py index 15a7a2b..7dab162 100644 --- a/ex_app/lib/all_tools/files.py +++ b/ex_app/lib/all_tools/files.py @@ -2,8 +2,9 @@ # SPDX-License-Identifier: AGPL-3.0-or-later from langchain_core.tools import tool from nc_py_api import Nextcloud +import niquests -from typing import Optional +from ex_app.lib.all_tools.lib.files import get_file_id_from_file_url from ex_app.lib.all_tools.lib.decorator import safe_tool, dangerous_tool @@ -27,6 +28,28 @@ def get_file_content(file_path: str): return response.text + @tool + @safe_tool + def get_file_content_by_file_link(file_url: str): + """ + Get the content of a file given an internal Nextcloud link (e.g., https://host/index.php/f/12345) + :param file_url: the internal file URL + :return: + """ + + file_id = get_file_id_from_file_url(file_url) + # Generate a direct download link using the fileId + info = nc.ocs('POST', '/ocs/v2.php/apps/dav/api/v1/direct', json={'fileId': file_id}, response_type='json') + download_url = info.get('url') if isinstance(info, dict) else None + + if not download_url: + raise Exception('Could not generate download URL from file id') + + # Download the file from the direct download URL + response = niquests.get(download_url) + + return response.text + @tool @safe_tool def get_folder_tree(depth: int): @@ -56,6 +79,7 @@ def create_public_sharing_link(path: str): return [ get_file_content, + get_file_content_by_file_link, get_folder_tree, create_public_sharing_link, ] diff --git a/ex_app/lib/all_tools/image_gen.py b/ex_app/lib/all_tools/image_gen.py index 979ae49..eef3532 100644 --- a/ex_app/lib/all_tools/image_gen.py +++ b/ex_app/lib/all_tools/image_gen.py @@ -3,7 +3,6 @@ from langchain_core.tools import tool from nc_py_api import Nextcloud -from ex_app.lib.all_tools.lib.files import get_file_id_from_file_url from ex_app.lib.all_tools.lib.task_processing import run_task from ex_app.lib.all_tools.lib.decorator import safe_tool @@ -14,7 +13,7 @@ async def get_tools(nc: Nextcloud): @safe_tool def generate_image(input: str) -> str: """ - Generate an image with the input string as description + Generate an image using AI with the input string as description :param text: the instructions for the image generation :return: a download link to the generated image """ diff --git a/ex_app/lib/all_tools/mail.py b/ex_app/lib/all_tools/mail.py index c37109e..f4b964a 100644 --- a/ex_app/lib/all_tools/mail.py +++ b/ex_app/lib/all_tools/mail.py @@ -16,11 +16,11 @@ async def get_tools(nc: Nextcloud): @dangerous_tool def send_email(subject: str, body: str, account_id: int, from_email: str, to_emails: list[str]): """ - Send an email to a list of emails + Send an email to a list of email addresses :param subject: The subject of the email :param body: The body of the email :param account_id: The id of the account to send from, obtainable via get_mail_account_list - :param to_emails: The emails to send + :param to_emails: The email addresses to send the message to """ i = 0 body_with_ai_note = f"{body}\n\n---\n\nThis email was sent by Nextcloud AI Assistant." @@ -47,7 +47,7 @@ def send_email(subject: str, body: str, account_id: int, from_email: str, to_ema @safe_tool def get_mail_account_list(): """ - Lists all available email accounts including their account id + Lists all available email accounts of the current user including their account id :param subject: The subject of the email :param body: The body of the email :param account_id: The id of the account to send from diff --git a/ex_app/lib/all_tools/talk.py b/ex_app/lib/all_tools/talk.py index 814f941..355e376 100644 --- a/ex_app/lib/all_tools/talk.py +++ b/ex_app/lib/all_tools/talk.py @@ -12,7 +12,7 @@ async def get_tools(nc: Nextcloud): @safe_tool def list_talk_conversations(): """ - List all conversations in talk + List all conversations of the user in the Nextcloud Talk app :return: returns a list of conversation names, e.g. ["Conversation 1", "Conversation 2"] """ conversations = nc.talk.get_user_conversations() @@ -23,7 +23,7 @@ def list_talk_conversations(): @dangerous_tool def create_public_conversation(conversation_name: str) -> str: """ - Create a new talk conversation + Create a new conversation in the Nextcloud Talk app :param conversation_name: The name of the conversation to create :return: The URL of the new conversation """ @@ -36,7 +36,7 @@ def create_public_conversation(conversation_name: str) -> str: @dangerous_tool def send_message_to_conversation(conversation_name: str, message: str): """ - List all conversations in talk + List all conversations in the Nextcloud talk app :param message: The message to send :param conversation_name: The name of the conversation to send a message to :return: @@ -52,7 +52,7 @@ def send_message_to_conversation(conversation_name: str, message: str): @safe_tool def list_messages_in_conversation(conversation_name: str, n_messages: int = 30): """ - List messages of a conversation in talk + List messages of a conversation in the Nextcloud Talk app :param conversation_name: The name of the conversation to list messages of (can only be one conversation per Tool call, obtainable via list_talk_conversations) :param n_messages: The number of messages to receive :return: A list of messages From 92ac33ab0588891a5ab082f05d72dac66958f2ca Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 5 Feb 2026 13:10:46 +0100 Subject: [PATCH 2/7] fix: Apply review suggestion Signed-off-by: Marcel Klehr --- ex_app/lib/all_tools/context_chat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ex_app/lib/all_tools/context_chat.py b/ex_app/lib/all_tools/context_chat.py index 91aa996..3c31b5f 100644 --- a/ex_app/lib/all_tools/context_chat.py +++ b/ex_app/lib/all_tools/context_chat.py @@ -13,7 +13,7 @@ async def get_tools(nc: Nextcloud): @safe_tool def ask_context_chat(question: str) -> str: """ - Ask the context chat oracle a question about the user's documents. It know the contents of all of the users documents. + Ask the context chat oracle a question about the user's documents. It knows the contents of all of the users documents. :param question: The question to ask :return: the answer from context chat """ From 317a4ab169aae165237dfd9e6a0ab9584948f87c Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 5 Feb 2026 13:22:06 +0100 Subject: [PATCH 3/7] fix: Fix get_file_content_by_file_link tool Signed-off-by: Marcel Klehr --- ex_app/lib/all_tools/files.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ex_app/lib/all_tools/files.py b/ex_app/lib/all_tools/files.py index 7dab162..ef397cb 100644 --- a/ex_app/lib/all_tools/files.py +++ b/ex_app/lib/all_tools/files.py @@ -34,13 +34,13 @@ def get_file_content_by_file_link(file_url: str): """ Get the content of a file given an internal Nextcloud link (e.g., https://host/index.php/f/12345) :param file_url: the internal file URL - :return: + :return: text content of the file """ file_id = get_file_id_from_file_url(file_url) # Generate a direct download link using the fileId info = nc.ocs('POST', '/ocs/v2.php/apps/dav/api/v1/direct', json={'fileId': file_id}, response_type='json') - download_url = info.get('url') if isinstance(info, dict) else None + download_url = info.get('ocs', {}).get('data', {}).get('url', None) if not download_url: raise Exception('Could not generate download URL from file id') From 5ca52fe8e872bd40f8d4596a05c64500d38ebeb5 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 5 Feb 2026 17:29:03 +0100 Subject: [PATCH 4/7] fix: Apply suggestion from @julien-nc Co-authored-by: Julien Veyssier Signed-off-by: Marcel Klehr --- ex_app/lib/all_tools/deck.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ex_app/lib/all_tools/deck.py b/ex_app/lib/all_tools/deck.py index 8fd5545..0671943 100644 --- a/ex_app/lib/all_tools/deck.py +++ b/ex_app/lib/all_tools/deck.py @@ -13,7 +13,7 @@ async def get_tools(nc: Nextcloud): @safe_tool def list_boards(): """ - List all existing kanban boards available in the Nextcloud Deck app to the current user with their available info + List all existing kanban boards available in the Nextcloud Deck app for the current user with their available info :return: a dictionary with all decks of the user """ From ac1a8977e4eff60dba96d5fa53b9cbda94f6df92 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 5 Feb 2026 17:29:24 +0100 Subject: [PATCH 5/7] fix: Apply suggestion from @julien-nc Co-authored-by: Julien Veyssier Signed-off-by: Marcel Klehr --- ex_app/lib/all_tools/image_gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ex_app/lib/all_tools/image_gen.py b/ex_app/lib/all_tools/image_gen.py index eef3532..6da44a0 100644 --- a/ex_app/lib/all_tools/image_gen.py +++ b/ex_app/lib/all_tools/image_gen.py @@ -13,7 +13,7 @@ async def get_tools(nc: Nextcloud): @safe_tool def generate_image(input: str) -> str: """ - Generate an image using AI with the input string as description + Generate an image using AI from a text description input :param text: the instructions for the image generation :return: a download link to the generated image """ From 244cbd098640de1af8178f1a9e568b141842106d Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 5 Feb 2026 17:29:44 +0100 Subject: [PATCH 6/7] fix: pply suggestion from @julien-nc Co-authored-by: Julien Veyssier Signed-off-by: Marcel Klehr --- ex_app/lib/all_tools/talk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ex_app/lib/all_tools/talk.py b/ex_app/lib/all_tools/talk.py index 355e376..b3e9407 100644 --- a/ex_app/lib/all_tools/talk.py +++ b/ex_app/lib/all_tools/talk.py @@ -12,7 +12,7 @@ async def get_tools(nc: Nextcloud): @safe_tool def list_talk_conversations(): """ - List all conversations of the user in the Nextcloud Talk app + List all conversations of the current user in the Nextcloud Talk app :return: returns a list of conversation names, e.g. ["Conversation 1", "Conversation 2"] """ conversations = nc.talk.get_user_conversations() From f2d2e8cf65ec31abcf3c3181e6653febfa47c25c Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 5 Feb 2026 17:30:44 +0100 Subject: [PATCH 7/7] fix: pply suggestion from @marcelklehr Signed-off-by: Marcel Klehr --- ex_app/lib/all_tools/talk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ex_app/lib/all_tools/talk.py b/ex_app/lib/all_tools/talk.py index b3e9407..bd968da 100644 --- a/ex_app/lib/all_tools/talk.py +++ b/ex_app/lib/all_tools/talk.py @@ -36,7 +36,7 @@ def create_public_conversation(conversation_name: str) -> str: @dangerous_tool def send_message_to_conversation(conversation_name: str, message: str): """ - List all conversations in the Nextcloud talk app + Send a message to a conversation in the Nextcloud talk app :param message: The message to send :param conversation_name: The name of the conversation to send a message to :return: