From ed2641545057a5c9b70b63f48e64a2284c591b9a Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Mon, 2 Mar 2026 15:46:19 +0100 Subject: [PATCH 1/5] feat(search): Add a tool for each unified search provider available in Nextcloud Signed-off-by: Marcel Klehr --- ex_app/lib/all_tools/search.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 ex_app/lib/all_tools/search.py diff --git a/ex_app/lib/all_tools/search.py b/ex_app/lib/all_tools/search.py new file mode 100644 index 0000000..0e2a37e --- /dev/null +++ b/ex_app/lib/all_tools/search.py @@ -0,0 +1,31 @@ +import json + +from langchain_core.tools import tool +from nc_py_api import AsyncNextcloudApp + +from ex_app.lib.all_tools.lib.decorator import safe_tool + +async def get_tools(nc: AsyncNextcloudApp): + tools = [] + providers = await nc.ocs('GET', '/ocs/v2.php/search/providers') + + for provider in providers: + def make_tool(p): + async def tool(search_query: dict[str, str]): + results = await nc.ocs('GET', f'/ocs/v2.php/search/providers/{p['id']}/search', params=search_query) + return json.dumps(results['entries']) + return tool + + tool_func = make_tool(provider) + tool_func.__name__ = "search_" + provider['name'].lower() + tool_func.__doc__ = f"Searches {provider['name']} in Nextcloud\n:param search_query: A list of filters to use for searches. Choose filters from {json.dumps(provider['filters'])}" + \ + ' e.g. in the form of {"term": "hans", ...}\n' + tools.append(tool(safe_tool(tool_func))) + + return tools + +def get_category_name(): + return "Unified Search" + +async def is_available(nc: AsyncNextcloudApp): + return True From fff70a91edb1637da9ee3b7e4c885bfc5652bc4c Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Mon, 2 Mar 2026 18:30:31 +0100 Subject: [PATCH 2/5] fix: Adress review comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Marcel Klehr --- ex_app/lib/all_tools/search.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ex_app/lib/all_tools/search.py b/ex_app/lib/all_tools/search.py index 0e2a37e..2f9e639 100644 --- a/ex_app/lib/all_tools/search.py +++ b/ex_app/lib/all_tools/search.py @@ -18,8 +18,12 @@ async def tool(search_query: dict[str, str]): tool_func = make_tool(provider) tool_func.__name__ = "search_" + provider['name'].lower() - tool_func.__doc__ = f"Searches {provider['name']} in Nextcloud\n:param search_query: A list of filters to use for searches. Choose filters from {json.dumps(provider['filters'])}" + \ - ' e.g. in the form of {"term": "hans", ...}\n' + tool_func.__doc__ = ( + f"Searches {provider['name']} in Nextcloud.\n" + f":param search_query: A mapping of filter names to filter values to use for the search. " + f"Choose filters from {json.dumps(provider['filters'])}. " + 'For example: {"term": "hans", ...}\n' + ) tools.append(tool(safe_tool(tool_func))) return tools From 027d71f93be258629da7eea1f7069c90551b0b28 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Mon, 2 Mar 2026 18:32:23 +0100 Subject: [PATCH 3/5] fix: Address review comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Marcel Klehr --- ex_app/lib/all_tools/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ex_app/lib/all_tools/search.py b/ex_app/lib/all_tools/search.py index 2f9e639..ba3acf7 100644 --- a/ex_app/lib/all_tools/search.py +++ b/ex_app/lib/all_tools/search.py @@ -12,7 +12,7 @@ async def get_tools(nc: AsyncNextcloudApp): for provider in providers: def make_tool(p): async def tool(search_query: dict[str, str]): - results = await nc.ocs('GET', f'/ocs/v2.php/search/providers/{p['id']}/search', params=search_query) + results = await nc.ocs('GET', f"/ocs/v2.php/search/providers/{p['id']}/search", params=search_query) return json.dumps(results['entries']) return tool From e56a2bf67ff7e0dd9ed6e0109877f0eed31f1ce3 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Wed, 4 Mar 2026 12:45:26 +0100 Subject: [PATCH 4/5] chore: Add missing license header Signed-off-by: Marcel Klehr --- ex_app/lib/all_tools/search.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ex_app/lib/all_tools/search.py b/ex_app/lib/all_tools/search.py index ba3acf7..9ee7def 100644 --- a/ex_app/lib/all_tools/search.py +++ b/ex_app/lib/all_tools/search.py @@ -1,3 +1,5 @@ +# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: AGPL-3.0-or-later import json from langchain_core.tools import tool From 5631a67078e7571b8f3d3d4233e20ccb3a0c3e01 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Thu, 5 Mar 2026 10:49:56 +0100 Subject: [PATCH 5/5] fix(ci): Fix integration test Use get_folder_tree instead of youtube_search Signed-off-by: Marcel Klehr --- .github/workflows/integration_test.yml | 5 ++--- ex_app/lib/all_tools/files.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index c70ac0b..ed4bade 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -192,7 +192,7 @@ jobs: CREDS: "alice:alice" run: | sleep 300 - TASK=$(curl -X POST -u "$CREDS" -H "oCS-APIRequest: true" -H "Content-type: application/json" http://localhost:8080/ocs/v2.php/taskprocessing/schedule?format=json --data-raw '{"input": {"input": "Search youtube for videos about Nextcloud", "confirmation":1, "conversation_token": ""},"type":"core:contextagent:interaction", "appId": "test", "customId": ""}') + TASK=$(curl -X POST -u "$CREDS" -H "oCS-APIRequest: true" -H "Content-type: application/json" http://localhost:8080/ocs/v2.php/taskprocessing/schedule?format=json --data-raw '{"input": {"input": "List the files I have in Nextcloud", "confirmation":1, "conversation_token": ""},"type":"core:contextagent:interaction", "appId": "test", "customId": ""}') echo $TASK TASK_ID=$(echo $TASK | jq '.ocs.data.task.id') NEXT_WAIT_TIME=0 @@ -209,8 +209,7 @@ jobs: [ "$TASK_STATUS" == '"STATUS_SUCCESSFUL"' ] echo $TASK | jq '.ocs.data.task.output.output' echo $TASK | jq '.ocs.data.task.output.sources' - echo $TASK | jq '.ocs.data.task.output.sources' | grep -q 'youtube_search' - echo $TASK | jq '.ocs.data.task.output.output' | grep -q 'Nextcloud' + echo $TASK | jq '.ocs.data.task.output.sources' | grep -q 'get_folder_tree' - name: Show nextcloud logs if: always() diff --git a/ex_app/lib/all_tools/files.py b/ex_app/lib/all_tools/files.py index 5f68b62..304a38c 100644 --- a/ex_app/lib/all_tools/files.py +++ b/ex_app/lib/all_tools/files.py @@ -54,7 +54,7 @@ async def get_file_content_by_file_link(file_url: str): @safe_tool async def get_folder_tree(depth: int): """ - Get the folder tree of the user + Get the folder tree of the user (lists the files the user has in Nextcloud Files) :param depth: the depth of the returned folder tree :return: """