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
3 changes: 0 additions & 3 deletions ex_app/lib/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
from ex_app.lib.tools import get_tools
from ex_app.lib.memorysaver import MemorySaver

from langchain_community.tools import YouTubeSearchTool, DuckDuckGoSearchResults

# Dummy thread id as we return the whole state
thread = {"configurable": {"thread_id": "thread-1"}}
Expand Down Expand Up @@ -47,8 +46,6 @@ def export_conversation(checkpointer):

def react(task, nc: Nextcloud):
safe_tools, dangerous_tools = get_tools(nc)
safe_tools.append(YouTubeSearchTool())
safe_tools.append(DuckDuckGoSearchResults(output_format="list"))

model.bind_nextcloud(nc)

Expand Down
66 changes: 0 additions & 66 deletions ex_app/lib/all_tools/ai.py

This file was deleted.

36 changes: 36 additions & 0 deletions ex_app/lib/all_tools/audio2text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
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


def get_tools(nc: Nextcloud):

@tool
@safe_tool
def transcribe_file(file_url: str) -> str:
"""
Transcribe a media file stored inside the 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
"""
task_input = {
'input': get_file_id_from_file_url(file_url),
}
task_output = run_task(nc, "core:audio2text", task_input).output
return task_output['output']

return [
transcribe_file,
]

def get_category_name():
return "Audio transcription"

def is_available(nc: Nextcloud):
tasktypes = nc.ocs('GET', '/ocs/v2.php/taskprocessing/tasktypes')['types'].keys()
return 'core:audio2text' in tasktypes
12 changes: 10 additions & 2 deletions ex_app/lib/all_tools/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
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.all_tools.lib.decorator import safe_tool, dangerous_tool, timed_memoize
from ex_app.lib.all_tools.lib.freebusy_finder import find_available_slots, round_to_nearest_half_hour
from ex_app.lib.logger import log

Expand Down Expand Up @@ -239,4 +239,12 @@ def add_task(calendar_name: str, title: str, description: str, due_date: Optiona
schedule_event,
find_free_time_slot_in_calendar,
add_task
]
]

def get_category_name():
return "Calendar and Tasks"

@timed_memoize(5*60)
def is_available(nc: Nextcloud):
print('SHOULD DISAPPEAR')
return 'calendar' in nc.apps.get_list()
8 changes: 7 additions & 1 deletion ex_app/lib/all_tools/contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,10 @@ def find_details_of_current_user() -> dict[str, typing.Any]:

return [
find_person_in_contacts, find_details_of_current_user
]
]

def get_category_name():
return "Contacts"

def is_available(nc: Nextcloud):
return True
38 changes: 38 additions & 0 deletions ex_app/lib/all_tools/context_chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
from langchain_core.tools import tool
from nc_py_api import Nextcloud

from ex_app.lib.all_tools.lib.task_processing import run_task
from ex_app.lib.all_tools.lib.decorator import safe_tool


def get_tools(nc: Nextcloud):

@tool
@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
:param question: The question to ask
:return: the answer from context chat
"""

task_input = {
'prompt': question,
'scopeType': 'none',
'scopeList': [],
'scopeListMeta': '',
}
task_output = run_task(nc, "context_chat:context_chat", task_input).output
return task_output['output']

return [
ask_context_chat,
]

def get_category_name():
return "Context chat"

def is_available(nc: Nextcloud):
return 'context_chat' in nc.apps.get_list()
8 changes: 7 additions & 1 deletion ex_app/lib/all_tools/deck.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,10 @@ def add_card(deck_id: int, stack_id: int, title: str):
return [
list_decks,
add_card
]
]

def get_category_name():
return "Deck"

def is_available(nc: Nextcloud):
return 'deck' in nc.capabilities
36 changes: 36 additions & 0 deletions ex_app/lib/all_tools/doc-gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
from langchain_core.tools import tool
from nc_py_api import Nextcloud

from ex_app.lib.all_tools.lib.task_processing import run_task
from ex_app.lib.all_tools.lib.decorator import safe_tool


def get_tools(nc: Nextcloud):

@tool
@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
:param format: the format of the generated file, available are "text_document" and "spreadsheet_document"
:return: a download link to the generated document
"""
tasktype = "richdocuments:text_to_" + format
task_input = {
'text': input,
}
task = run_task(nc, tasktype, task_input)
return f"https://nextcloud.local/ocs/v2.php/apps/assistant/api/v1/task/{task.id}/output-file/{task.output['file']}/download"

return [
generate_document,
]

def get_category_name():
return "Office document generation"

def is_available(nc: Nextcloud):
return 'richdocuments' in nc.capabilities
21 changes: 21 additions & 0 deletions ex_app/lib/all_tools/duckduckgo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
from langchain_core.tools import tool
from nc_py_api import Nextcloud
from langchain_community.tools import DuckDuckGoSearchResults

from ex_app.lib.all_tools.lib.decorator import safe_tool


def get_tools(nc: Nextcloud):

web_search = DuckDuckGoSearchResults(output_format="list")
return [
web_search,
]

def get_category_name():
return "DuckDuckGo"

def is_available(nc: Nextcloud):
return True
8 changes: 7 additions & 1 deletion ex_app/lib/all_tools/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,10 @@ def create_public_sharing_link(path: str):
get_file_content,
get_folder_tree,
create_public_sharing_link,
]
]

def get_category_name():
return "Files"

def is_available(nc: Nextcloud):
return True
47 changes: 47 additions & 0 deletions ex_app/lib/all_tools/here.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
import typing
import datetime
import urllib.parse

import httpx
from langchain_core.tools import tool
from nc_py_api import Nextcloud

from ex_app.lib.all_tools.lib.decorator import safe_tool


def get_tools(nc: Nextcloud):

@tool
@safe_tool
def get_public_transport_route_for_coordinates(origin_lat: str, origin_lon: str, destination_lat: str, destination_lon: str, routes: int, departure_time: str | None = None):
"""
Retrieve a public transport route between two coordinates
:param origin_lat: Latitude of the starting point
:param origin_lon: Longitude of the starting point
:param destination_lat: Latitude of the destination
:param destination_lon: Longitude of the destination
:param routes: the number of routes returned
:param departure_time: time of departure, formatted like '2019-06-24T01:23:45'. Optional, leave empty for the next routes from now
:return: the routes, times are given in local time according to origin and destination
"""

if departure_time is None:
departure_time = urllib.parse.quote_plus(datetime.datetime.now(datetime.UTC).isoformat())
api_key = nc.appconfig_ex.get_value('here_api')
res = httpx.get('https://transit.hereapi.com/v8/routes?transportMode=car&origin='
+ origin_lat + ',' + origin_lon + '&destination=' + destination_lat + ',' + destination_lon
+ '&alternatives=' + str(routes-1) + '&departureTime=' + departure_time + '&apikey=' + api_key)
json = res.json()
return json

return [
get_public_transport_route_for_coordinates,
]

def get_category_name():
return "Public transport"

def is_available(nc: Nextcloud):
return nc.appconfig_ex.get_value('here_api') != ''
30 changes: 29 additions & 1 deletion ex_app/lib/all_tools/lib/decorator.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
import functools
import time
from functools import wraps

def safe_tool(tool):
setattr(tool, 'safe', True)
return tool

def dangerous_tool(tool):
setattr(tool, 'safe', False)
return tool
return tool

def timed_memoize(timeout):
def decorator(func):
cached_result = None
timestamp = 0

@wraps(func)
def wrapper(*args):
nonlocal cached_result
nonlocal timestamp
current_time = time.time()
if cached_result != None:
if current_time - timestamp < timeout:
return cached_result
else:
# Cache expired
cached_result = None
timestamp = 0
# Call the function and cache the result
result = func(*args)
cached_result = result
timestamp = current_time
return result

return wrapper
return decorator
8 changes: 7 additions & 1 deletion ex_app/lib/all_tools/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,10 @@ def get_mail_account_list():
return [
send_email,
get_mail_account_list
]
]

def get_category_name():
return "Mail"

def is_available(nc: Nextcloud):
return 'mail' in nc.apps.get_list()
Loading
Loading