Skip to content

Commit 7af57d6

Browse files
authored
Merge pull request #157 from Pavlinchen/feat/talk-reactions-polls-files
feat(talk): Add reactions, replies, polls, and file sharing tools
2 parents fa4f978 + e34627f commit 7af57d6

1 file changed

Lines changed: 242 additions & 18 deletions

File tree

ex_app/lib/all_tools/talk.py

Lines changed: 242 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
22
# SPDX-License-Identifier: AGPL-3.0-or-later
3+
import json
4+
from typing import Optional
35
from langchain_core.tools import tool
46
from nc_py_api import AsyncNextcloudApp
57
from nc_py_api.talk import ConversationType
@@ -8,64 +10,286 @@
810

911

1012
async def get_tools(nc: AsyncNextcloudApp):
13+
14+
async def _get_token(conversation_name: str) -> str:
15+
conversations = await nc.talk.get_user_conversations()
16+
conv_map = {conv.display_name: conv for conv in conversations}
17+
return conv_map[conversation_name].token
18+
19+
# --- Conversations & Messages (enhanced existing tools) ---
20+
1121
@tool
1222
@safe_tool
1323
async def list_talk_conversations():
1424
"""
15-
List all conversations of the current user in the Nextcloud Talk app
16-
:return: returns a list of conversation names, e.g. ["Conversation 1", "Conversation 2"]
25+
List all conversations of the current user in the Nextcloud Talk app.
26+
Returns conversation names and tokens. The token is needed for other Talk tools.
27+
:return: list of conversations with name, token, type, and unread message count
1728
"""
1829
conversations = await nc.talk.get_user_conversations()
19-
20-
return [conv.display_name for conv in conversations]
30+
return json.dumps([{
31+
'name': conv.display_name,
32+
'token': conv.token,
33+
'type': conv.conversation_type,
34+
'unread_messages': conv.unread_messages_count,
35+
} for conv in conversations])
2136

2237
@tool
2338
@dangerous_tool
2439
async def create_public_conversation(conversation_name: str) -> str:
2540
"""
26-
Create a new conversation in the Nextcloud Talk app
41+
Create a new public conversation in the Nextcloud Talk app
2742
:param conversation_name: The name of the conversation to create
2843
:return: The URL of the new conversation
2944
"""
3045
conversation = await nc.talk.create_conversation(ConversationType.PUBLIC, room_name=conversation_name)
31-
3246
return f"{nc.app_cfg.endpoint}/index.php/call/{conversation.token}"
3347

34-
3548
@tool
3649
@dangerous_tool
3750
async def send_message_to_conversation(conversation_name: str, message: str):
3851
"""
39-
Send a message to a conversation in the Nextcloud talk app
52+
Send a message to a conversation in the Nextcloud Talk app
4053
:param message: The message to send
41-
:param conversation_name: The name of the conversation to send a message to
42-
:return:
54+
:param conversation_name: The name of the conversation to send a message to (obtainable via list_talk_conversations)
55+
:return: success confirmation
4356
"""
4457
conversations = await nc.talk.get_user_conversations()
4558
conversation = {conv.display_name: conv for conv in conversations}[conversation_name]
4659
message_with_ai_note = f"{message}\n\nThis message was sent by Nextcloud AI Assistant."
4760
await nc.talk.send_message(message_with_ai_note, conversation)
48-
49-
return True
61+
return "Message sent successfully."
5062

5163
@tool
5264
@safe_tool
5365
async def list_messages_in_conversation(conversation_name: str, n_messages: int = 30):
5466
"""
55-
List messages of a conversation in the Nextcloud Talk app
56-
: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)
67+
List messages of a conversation in the Nextcloud Talk app.
68+
Each message includes its id (needed for reactions and replies) and whether it can be replied to.
69+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
5770
:param n_messages: The number of messages to receive
58-
:return: A list of messages
71+
:return: list of messages with id, timestamp, actor, message text, and reply status
5972
"""
6073
conversations = await nc.talk.get_user_conversations()
6174
conversation = {conv.display_name: conv for conv in conversations}[conversation_name]
62-
return [f"{m.timestamp} {m.actor_display_name}: {m.message}" for m in await nc.talk.receive_messages(conversation, False, n_messages)]
75+
messages = await nc.talk.receive_messages(conversation, False, n_messages)
76+
return json.dumps([{
77+
'id': m.message_id,
78+
'timestamp': m.timestamp,
79+
'actor': m.actor_display_name,
80+
'message': m.message,
81+
'is_replyable': m.is_replyable,
82+
'reactions': m.reactions,
83+
} for m in messages])
84+
85+
# --- Reactions ---
86+
87+
@tool
88+
@dangerous_tool
89+
async def add_reaction(conversation_name: str, message_id: int, reaction: str):
90+
"""
91+
Add an emoji reaction to a message in a Talk conversation
92+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
93+
:param message_id: The id of the message to react to (obtainable via list_messages_in_conversation)
94+
:param reaction: The reaction emoji (e.g. "\U0001f44d", "❤️", "\U0001f389")
95+
:return: all reactions on the message grouped by emoji
96+
"""
97+
token = await _get_token(conversation_name)
98+
return json.dumps(await nc.ocs('POST', f'/ocs/v2.php/apps/spreed/api/v1/reaction/{token}/{message_id}', json={
99+
'reaction': reaction,
100+
}))
101+
102+
@tool
103+
@dangerous_tool
104+
async def remove_reaction(conversation_name: str, message_id: int, reaction: str):
105+
"""
106+
Remove an emoji reaction from a message in a Talk conversation.
107+
Only your own reactions can be removed.
108+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
109+
:param message_id: The id of the message (obtainable via list_messages_in_conversation)
110+
:param reaction: The reaction emoji to remove
111+
:return: remaining reactions on the message grouped by emoji
112+
"""
113+
token = await _get_token(conversation_name)
114+
return json.dumps(await nc.ocs('DELETE', f'/ocs/v2.php/apps/spreed/api/v1/reaction/{token}/{message_id}', json={
115+
'reaction': reaction,
116+
}))
117+
118+
@tool
119+
@safe_tool
120+
async def list_reactions(conversation_name: str, message_id: int, reaction: Optional[str] = None):
121+
"""
122+
List all reactions on a message in a Talk conversation
123+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
124+
:param message_id: The id of the message (obtainable via list_messages_in_conversation)
125+
:param reaction: Optional emoji to filter for a specific reaction
126+
:return: reactions grouped by emoji, each containing a list of actors who reacted
127+
"""
128+
token = await _get_token(conversation_name)
129+
params = {}
130+
if reaction is not None:
131+
params['reaction'] = reaction
132+
return json.dumps(await nc.ocs('GET', f'/ocs/v2.php/apps/spreed/api/v1/reaction/{token}/{message_id}', params=params))
133+
134+
# --- Reply to message ---
135+
136+
@tool
137+
@dangerous_tool
138+
async def reply_to_message(conversation_name: str, message_id: int, message: str, silent: bool = False):
139+
"""
140+
Send a message as a reply to another message in a Talk conversation.
141+
The reply will be visually linked to the original message.
142+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
143+
:param message_id: The id of the message to reply to (must have is_replyable=true, obtainable via list_messages_in_conversation)
144+
:param message: The reply text
145+
:param silent: If true, no chat notifications will be sent (default false)
146+
:return: the sent message with its id and parent reference
147+
"""
148+
token = await _get_token(conversation_name)
149+
message_with_ai_note = f"{message}\n\nThis message was sent by Nextcloud AI Assistant."
150+
return json.dumps(await nc.ocs('POST', f'/ocs/v2.php/apps/spreed/api/v1/chat/{token}', json={
151+
'message': message_with_ai_note,
152+
'replyTo': message_id,
153+
'silent': silent,
154+
}))
155+
156+
# --- Polls ---
157+
158+
@tool
159+
@dangerous_tool
160+
async def create_poll(conversation_name: str, question: str, options: list[str], result_mode: int = 0, max_votes: int = 0):
161+
"""
162+
Create a poll in a Talk conversation
163+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
164+
:param question: The poll question
165+
:param options: List of voting options (e.g. ["Yes", "No", "Maybe"])
166+
:param result_mode: 0 = public (results visible immediately), 1 = hidden (results shown only after closing). Default 0.
167+
:param max_votes: Maximum options a participant can vote for (0 = unlimited). Default 0.
168+
:return: the created poll with its id, question, options, and status
169+
"""
170+
token = await _get_token(conversation_name)
171+
return json.dumps(await nc.ocs('POST', f'/ocs/v2.php/apps/spreed/api/v1/poll/{token}', json={
172+
'question': question,
173+
'options': options,
174+
'resultMode': result_mode,
175+
'maxVotes': max_votes,
176+
}))
177+
178+
@tool
179+
@safe_tool
180+
async def get_poll(conversation_name: str, poll_id: int):
181+
"""
182+
Get the current state and results of a poll
183+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
184+
:param poll_id: The id of the poll (obtainable from create_poll or from message parameters in list_messages_in_conversation)
185+
:return: poll data including question, options (0-based, e.g. [0] refers to the first option, [2] refers to the third option, etc.), votes, status, and who voted
186+
"""
187+
token = await _get_token(conversation_name)
188+
return json.dumps(await nc.ocs('GET', f'/ocs/v2.php/apps/spreed/api/v1/poll/{token}/{poll_id}'))
189+
190+
@tool
191+
@dangerous_tool
192+
async def vote_on_poll(conversation_name: str, poll_id: int, option_ids: list[int]):
193+
"""
194+
Vote on a poll in a Talk conversation.
195+
Voting replaces any previous votes by the current user.
196+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
197+
:param poll_id: The id of the poll
198+
:param option_ids: List of option indices to vote for (0-based, e.g. [0] to vote for the first option, [0, 2] to vote for first and third)
199+
:return: updated poll data with vote counts and own votes
200+
"""
201+
token = await _get_token(conversation_name)
202+
return json.dumps(await nc.ocs('POST', f'/ocs/v2.php/apps/spreed/api/v1/poll/{token}/{poll_id}', json={
203+
'optionIds': option_ids,
204+
}))
205+
206+
@tool
207+
@dangerous_tool
208+
async def close_poll(conversation_name: str, poll_id: int):
209+
"""
210+
Close a poll so no more votes can be cast. Only the poll creator or a moderator can close a poll.
211+
Once closed, full results become visible to all participants.
212+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
213+
:param poll_id: The id of the poll to close
214+
:return: final poll data with complete vote counts and details
215+
"""
216+
token = await _get_token(conversation_name)
217+
return json.dumps(await nc.ocs('DELETE', f'/ocs/v2.php/apps/spreed/api/v1/poll/{token}/{poll_id}'))
218+
219+
# --- File sharing ---
220+
221+
@tool
222+
@dangerous_tool
223+
async def share_file_to_conversation(conversation_name: str, file_path: str, caption: Optional[str] = None):
224+
"""
225+
Share a file from Nextcloud Files into a Talk conversation.
226+
The file will appear as a rich message in the chat.
227+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
228+
:param file_path: Path to the file in the user's Nextcloud files (e.g. "/Documents/report.pdf")
229+
:param caption: Optional caption text to display with the shared file
230+
:return: the created share
231+
"""
232+
token = await _get_token(conversation_name)
233+
payload = {
234+
'shareType': 10,
235+
'shareWith': token,
236+
'path': file_path,
237+
}
238+
if caption is not None:
239+
caption_with_ai_note = f"{caption}\n\nShared by Nextcloud AI Assistant."
240+
payload['talkMetaData'] = json.dumps({'caption': caption_with_ai_note})
241+
return json.dumps(await nc.ocs('POST', '/ocs/v2.php/apps/files_sharing/api/v1/shares', json=payload))
242+
243+
@tool
244+
@safe_tool
245+
async def list_shared_items(conversation_name: str, object_type: str, limit: int = 100):
246+
"""
247+
List items of a specific type that have been shared in a Talk conversation.
248+
For a grouped overview across all types, use list_shared_items_overview instead.
249+
Note: polls have their own dedicated tools (get_poll, vote_on_poll); for polls the
250+
dedicated tools are usually more direct than listing them through this one.
251+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
252+
:param object_type: The kind of shared item to list. One of "file", "media",
253+
"audio", "voice", "poll", "location", "deckcard", "recording", "o-talk", "other".
254+
:param limit: Maximum number of results (default 100, max 200)
255+
:return: list of chat messages containing shared items of the requested type with their metadata
256+
"""
257+
token = await _get_token(conversation_name)
258+
return json.dumps(await nc.ocs('GET', f'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/share', params={
259+
'objectType': object_type,
260+
'limit': limit,
261+
}))
262+
263+
@tool
264+
@safe_tool
265+
async def list_shared_items_overview(conversation_name: str, limit: int = 7):
266+
"""
267+
Get an overview of all types of shared items in a Talk conversation (files, media, polls, etc.)
268+
:param conversation_name: The name of the conversation (obtainable via list_talk_conversations)
269+
:param limit: Maximum items per category (default 7)
270+
:return: shared items grouped by type (audio, file, media, poll, etc.)
271+
"""
272+
token = await _get_token(conversation_name)
273+
return json.dumps(await nc.ocs('GET', f'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/share/overview', params={
274+
'limit': limit,
275+
}))
63276

64277
return [
65278
list_talk_conversations,
66-
list_messages_in_conversation,
67-
send_message_to_conversation,
68279
create_public_conversation,
280+
send_message_to_conversation,
281+
list_messages_in_conversation,
282+
add_reaction,
283+
remove_reaction,
284+
list_reactions,
285+
reply_to_message,
286+
create_poll,
287+
get_poll,
288+
vote_on_poll,
289+
close_poll,
290+
share_file_to_conversation,
291+
list_shared_items,
292+
list_shared_items_overview,
69293
]
70294

71295
def get_category_name():

0 commit comments

Comments
 (0)