Skip to content

Commit 64e674c

Browse files
committed
Merge branch 'feat/moartools' of https://github.com/nextcloud/context_agent into copilot/test-new-tools-in-pr-127
2 parents b8a9d30 + 374a5ea commit 64e674c

15 files changed

Lines changed: 2219 additions & 13 deletions

File tree

ex_app/lib/all_tools/bookmarks.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
from typing import Optional
4+
from langchain_core.tools import tool
5+
from nc_py_api import AsyncNextcloudApp
6+
7+
from ex_app.lib.all_tools.lib.decorator import safe_tool, dangerous_tool
8+
9+
10+
async def get_tools(nc: AsyncNextcloudApp):
11+
@tool
12+
@safe_tool
13+
async def list_bookmarks(page: int = 0, limit: int = 100, folder_id: Optional[int] = None, tags: Optional[list[str]] = None):
14+
"""
15+
List bookmarks with optional filtering
16+
:param page: page number for pagination (starts at 0)
17+
:param limit: number of bookmarks per page (default 100)
18+
:param folder_id: filter by folder id (obtainable via list_bookmark_folders)
19+
:param tags: filter by tags - list of tag names
20+
:return: list of bookmarks with url, title, description, and tags
21+
"""
22+
params = {
23+
'page': page,
24+
'limit': limit
25+
}
26+
if folder_id is not None:
27+
params['folder'] = folder_id
28+
if tags:
29+
params['tags[]'] = tags
30+
31+
response = await nc._session._create_adapter(True).request('GET', f"{nc.app_cfg.endpoint}/index.php/apps/bookmarks/public/rest/v2/bookmark", headers={
32+
"Content-Type": "application/json",
33+
}, params=params)
34+
return response.json()
35+
36+
@tool
37+
@safe_tool
38+
async def search_bookmarks(search_term: str):
39+
"""
40+
Search for bookmarks by keyword
41+
:param search_term: text to search for in bookmark titles, urls, and descriptions
42+
:return: list of matching bookmarks
43+
"""
44+
response = await nc._session._create_adapter(True).request('GET', f"{nc.app_cfg.endpoint}/index.php/apps/bookmarks/public/rest/v2/bookmark", headers={
45+
"Content-Type": "application/json",
46+
}, params={'search': search_term})
47+
return response.json()
48+
49+
@tool
50+
@dangerous_tool
51+
async def create_bookmark(url: str, title: Optional[str] = None, description: Optional[str] = None, tags: Optional[list[str]] = None, folder_id: Optional[int] = None):
52+
"""
53+
Create a new bookmark
54+
:param url: the URL to bookmark
55+
:param title: title for the bookmark (auto-detected if not provided)
56+
:param description: optional description
57+
:param tags: list of tags to add to the bookmark
58+
:param folder_id: folder to place the bookmark in (obtainable via list_bookmark_folders)
59+
:return: the created bookmark
60+
"""
61+
description_with_ai_note = f"{description or ''}\n\nBookmarked by Nextcloud AI Assistant."
62+
63+
payload = {
64+
'url': url,
65+
'description': description_with_ai_note
66+
}
67+
if title:
68+
payload['title'] = title
69+
if tags:
70+
payload['tags'] = tags
71+
if folder_id is not None:
72+
payload['folders'] = [folder_id]
73+
74+
response = await nc._session._create_adapter(True).request('POST', f"{nc.app_cfg.endpoint}/index.php/apps/bookmarks/public/rest/v2/bookmark", headers={
75+
"Content-Type": "application/json",
76+
}, json=payload)
77+
return response.json()
78+
79+
@tool
80+
@dangerous_tool
81+
async def update_bookmark(bookmark_id: int, url: Optional[str] = None, title: Optional[str] = None, description: Optional[str] = None, tags: Optional[list[str]] = None):
82+
"""
83+
Update an existing bookmark
84+
:param bookmark_id: the id of the bookmark to update (obtainable via list_bookmarks)
85+
:param url: new URL
86+
:param title: new title
87+
:param description: new description
88+
:param tags: new list of tags (replaces existing tags)
89+
:return: the updated bookmark
90+
"""
91+
payload = {}
92+
if url is not None:
93+
payload['url'] = url
94+
if title is not None:
95+
payload['title'] = title
96+
if description is not None:
97+
payload['description'] = description
98+
if tags is not None:
99+
payload['tags'] = tags
100+
101+
response = await nc._session._create_adapter(True).request('PUT', f"{nc.app_cfg.endpoint}/index.php/apps/bookmarks/public/rest/v2/bookmark/{bookmark_id}", headers={
102+
"Content-Type": "application/json",
103+
}, json=payload)
104+
return response.json()
105+
106+
@tool
107+
@dangerous_tool
108+
async def delete_bookmark(bookmark_id: int):
109+
"""
110+
Delete a bookmark
111+
:param bookmark_id: the id of the bookmark to delete (obtainable via list_bookmarks)
112+
:return: confirmation of deletion
113+
"""
114+
response = await nc._session._create_adapter(True).request('DELETE', f"{nc.app_cfg.endpoint}/index.php/apps/bookmarks/public/rest/v2/bookmark/{bookmark_id}", headers={
115+
"Content-Type": "application/json",
116+
})
117+
return response.json()
118+
119+
@tool
120+
@safe_tool
121+
async def list_bookmark_folders():
122+
"""
123+
List all bookmark folders
124+
:return: list of folders with their id, title, and parent folder
125+
"""
126+
response = await nc._session._create_adapter(True).request('GET', f"{nc.app_cfg.endpoint}/index.php/apps/bookmarks/public/rest/v2/folder", headers={
127+
"Content-Type": "application/json",
128+
})
129+
return response.json()
130+
131+
@tool
132+
@dangerous_tool
133+
async def create_bookmark_folder(title: str, parent_folder_id: Optional[int] = None):
134+
"""
135+
Create a new bookmark folder
136+
:param title: name for the folder
137+
:param parent_folder_id: optional parent folder id to create a subfolder (obtainable via list_bookmark_folders)
138+
:return: the created folder
139+
"""
140+
payload = {
141+
'title': title
142+
}
143+
if parent_folder_id is not None:
144+
payload['parent_folder'] = parent_folder_id
145+
146+
response = await nc._session._create_adapter(True).request('POST', f"{nc.app_cfg.endpoint}/index.php/apps/bookmarks/public/rest/v2/folder", headers={
147+
"Content-Type": "application/json",
148+
}, json=payload)
149+
return response.json()
150+
151+
@tool
152+
@safe_tool
153+
async def list_bookmark_tags():
154+
"""
155+
List all bookmark tags with usage counts
156+
:return: list of tags with the number of bookmarks using each tag
157+
"""
158+
response = await nc._session._create_adapter(True).request('GET', f"{nc.app_cfg.endpoint}/index.php/apps/bookmarks/public/rest/v2/tag", headers={
159+
"Content-Type": "application/json",
160+
})
161+
return response.json()
162+
163+
return [
164+
list_bookmarks,
165+
search_bookmarks,
166+
create_bookmark,
167+
update_bookmark,
168+
delete_bookmark,
169+
list_bookmark_folders,
170+
create_bookmark_folder,
171+
list_bookmark_tags
172+
]
173+
174+
def get_category_name():
175+
return "Bookmarks"
176+
177+
async def is_available(nc: AsyncNextcloudApp):
178+
return 'bookmarks' in await nc.capabilities

ex_app/lib/all_tools/calendar.py

Lines changed: 189 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,199 @@ async def add_task(calendar_name: str, title: str, description: str, due_date: O
254254

255255
return True
256256

257+
def list_tasks_sync(calendar_name: Optional[str] = None):
258+
principal = ncSync.cal.principal()
259+
calendars = principal.calendars()
260+
261+
tasks = []
262+
if calendar_name:
263+
calendar = {cal.name: cal for cal in calendars}[calendar_name]
264+
calendars_to_check = [calendar]
265+
else:
266+
calendars_to_check = calendars
267+
268+
for cal in calendars_to_check:
269+
todos = cal.todos()
270+
for todo in todos:
271+
# Parse the todo data using ics library
272+
try:
273+
ical_data = todo.data
274+
parsed_cal = Calendar(ical_data)
275+
276+
for ics_todo in parsed_cal.todos:
277+
task_data = {
278+
'calendar': cal.name,
279+
'summary': ics_todo.name or '',
280+
'uid': ics_todo.uid or '',
281+
'status': ics_todo.status or 'NEEDS-ACTION',
282+
'due': str(ics_todo.due) if ics_todo.due else None,
283+
'priority': ics_todo.priority,
284+
'description': ics_todo.description or '',
285+
}
286+
tasks.append(task_data)
287+
except:
288+
# Fallback if parsing fails
289+
continue
290+
291+
return tasks
292+
293+
@tool
294+
@safe_tool
295+
async def list_tasks(calendar_name: Optional[str] = None, filter_status: Optional[str] = None):
296+
"""
297+
List tasks from calendars. Can filter by calendar name and status.
298+
:param calendar_name: Optional name of the calendar to list tasks from (obtainable via list_calendars). If not provided, lists from all calendars.
299+
:param filter_status: Optional filter by status - one of: 'NEEDS-ACTION', 'COMPLETED', 'IN-PROCESS', 'CANCELLED'
300+
:return: list of tasks with their details
301+
"""
302+
tasks = await asyncio.to_thread(list_tasks_sync, calendar_name)
303+
304+
if filter_status:
305+
tasks = [t for t in tasks if t.get('status') == filter_status]
306+
307+
return tasks
308+
309+
def complete_task_sync(calendar_name: str, task_uid: str):
310+
principal = ncSync.cal.principal()
311+
calendars = principal.calendars()
312+
calendar = {cal.name: cal for cal in calendars}[calendar_name]
313+
314+
todos = calendar.todos()
315+
for todo in todos:
316+
# Parse the todo data using ics library
317+
try:
318+
ical_data = todo.data
319+
parsed_cal = Calendar(ical_data)
320+
321+
for ics_todo in parsed_cal.todos:
322+
if ics_todo.uid == task_uid:
323+
# Mark as completed
324+
ics_todo.status = 'COMPLETED'
325+
ics_todo.completed = datetime.now(timezone.utc)
326+
327+
# Serialize and save
328+
todo.data = str(parsed_cal)
329+
todo.save()
330+
return True
331+
except:
332+
continue
333+
334+
return False
335+
336+
@tool
337+
@dangerous_tool
338+
async def complete_task(calendar_name: str, task_uid: str):
339+
"""
340+
Mark a task as completed
341+
:param calendar_name: The name of the calendar containing the task (obtainable via list_calendars)
342+
:param task_uid: The UID of the task to complete (obtainable via list_tasks)
343+
:return: bool indicating success
344+
"""
345+
return await asyncio.to_thread(complete_task_sync, calendar_name, task_uid)
346+
347+
def update_task_sync(calendar_name: str, task_uid: str, title: Optional[str] = None, description: Optional[str] = None, due_date: Optional[str] = None, due_time: Optional[str] = None, timezone_str: Optional[str] = None, priority: Optional[int] = None):
348+
principal = ncSync.cal.principal()
349+
calendars = principal.calendars()
350+
calendar = {cal.name: cal for cal in calendars}[calendar_name]
351+
352+
todos = calendar.todos()
353+
for todo in todos:
354+
# Parse the todo data using ics library
355+
try:
356+
ical_data = todo.data
357+
parsed_cal = Calendar(ical_data)
358+
359+
for ics_todo in parsed_cal.todos:
360+
if ics_todo.uid == task_uid:
361+
# Update fields if provided
362+
if title:
363+
ics_todo.name = title
364+
if description:
365+
ics_todo.description = description
366+
if priority is not None:
367+
ics_todo.priority = priority
368+
if due_date:
369+
parsed_date = datetime.strptime(due_date, "%Y-%m-%d")
370+
if due_time:
371+
parsed_time = datetime.strptime(due_time, "%I:%M %p").time()
372+
due_datetime = datetime.combine(parsed_date, parsed_time)
373+
else:
374+
due_datetime = parsed_date
375+
376+
if timezone_str:
377+
tz = pytz.timezone(timezone_str)
378+
due_datetime = tz.localize(due_datetime)
379+
380+
ics_todo.due = due_datetime
381+
382+
# Serialize and save
383+
todo.data = str(parsed_cal)
384+
todo.save()
385+
return True
386+
except:
387+
continue
388+
389+
return False
390+
391+
@tool
392+
@dangerous_tool
393+
async def update_task(calendar_name: str, task_uid: str, title: Optional[str] = None, description: Optional[str] = None, due_date: Optional[str] = None, due_time: Optional[str] = None, timezone: Optional[str] = None, priority: Optional[int] = None):
394+
"""
395+
Update an existing task
396+
:param calendar_name: The name of the calendar containing the task (obtainable via list_calendars)
397+
:param task_uid: The UID of the task to update (obtainable via list_tasks)
398+
:param title: New title for the task
399+
:param description: New description for the task
400+
:param due_date: New due date in the form: YYYY-MM-DD e.g. '2024-12-01'
401+
:param due_time: New due time in the form: HH:MM AM/PM e.g. '3:00 PM'
402+
:param timezone: Timezone (e.g., 'America/New_York')
403+
:param priority: Priority from 0 (undefined) to 9 (lowest), where 1 is highest priority
404+
:return: bool indicating success
405+
"""
406+
return await asyncio.to_thread(update_task_sync, calendar_name, task_uid, title, description, due_date, due_time, timezone, priority)
407+
408+
def delete_task_sync(calendar_name: str, task_uid: str):
409+
principal = ncSync.cal.principal()
410+
calendars = principal.calendars()
411+
calendar = {cal.name: cal for cal in calendars}[calendar_name]
412+
413+
todos = calendar.todos()
414+
for todo in todos:
415+
# Parse the todo data using ics library to find the right one
416+
try:
417+
ical_data = todo.data
418+
parsed_cal = Calendar(ical_data)
419+
420+
for ics_todo in parsed_cal.todos:
421+
if ics_todo.uid == task_uid:
422+
# Delete the todo
423+
todo.delete()
424+
return True
425+
except:
426+
continue
427+
428+
return False
429+
430+
@tool
431+
@dangerous_tool
432+
async def delete_task(calendar_name: str, task_uid: str):
433+
"""
434+
Delete a task
435+
:param calendar_name: The name of the calendar containing the task (obtainable via list_calendars)
436+
:param task_uid: The UID of the task to delete (obtainable via list_tasks)
437+
:return: bool indicating success
438+
"""
439+
return await asyncio.to_thread(delete_task_sync, calendar_name, task_uid)
440+
257441
return [
258442
list_calendars,
259443
schedule_event,
260444
find_free_time_slot_in_calendar,
261-
add_task
445+
add_task,
446+
list_tasks,
447+
complete_task,
448+
update_task,
449+
delete_task
262450
]
263451

264452
def get_category_name():

0 commit comments

Comments
 (0)