daita-agents 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- daita/__init__.py +216 -0
- daita/agents/__init__.py +33 -0
- daita/agents/base.py +743 -0
- daita/agents/substrate.py +1141 -0
- daita/cli/__init__.py +145 -0
- daita/cli/__main__.py +7 -0
- daita/cli/ascii_art.py +44 -0
- daita/cli/core/__init__.py +0 -0
- daita/cli/core/create.py +254 -0
- daita/cli/core/deploy.py +473 -0
- daita/cli/core/deployments.py +309 -0
- daita/cli/core/import_detector.py +219 -0
- daita/cli/core/init.py +481 -0
- daita/cli/core/logs.py +239 -0
- daita/cli/core/managed_deploy.py +709 -0
- daita/cli/core/run.py +648 -0
- daita/cli/core/status.py +421 -0
- daita/cli/core/test.py +239 -0
- daita/cli/core/webhooks.py +172 -0
- daita/cli/main.py +588 -0
- daita/cli/utils.py +541 -0
- daita/config/__init__.py +62 -0
- daita/config/base.py +159 -0
- daita/config/settings.py +184 -0
- daita/core/__init__.py +262 -0
- daita/core/decision_tracing.py +701 -0
- daita/core/exceptions.py +480 -0
- daita/core/focus.py +251 -0
- daita/core/interfaces.py +76 -0
- daita/core/plugin_tracing.py +550 -0
- daita/core/relay.py +779 -0
- daita/core/reliability.py +381 -0
- daita/core/scaling.py +459 -0
- daita/core/tools.py +554 -0
- daita/core/tracing.py +770 -0
- daita/core/workflow.py +1144 -0
- daita/display/__init__.py +1 -0
- daita/display/console.py +160 -0
- daita/execution/__init__.py +58 -0
- daita/execution/client.py +856 -0
- daita/execution/exceptions.py +92 -0
- daita/execution/models.py +317 -0
- daita/llm/__init__.py +60 -0
- daita/llm/anthropic.py +291 -0
- daita/llm/base.py +530 -0
- daita/llm/factory.py +101 -0
- daita/llm/gemini.py +355 -0
- daita/llm/grok.py +219 -0
- daita/llm/mock.py +172 -0
- daita/llm/openai.py +220 -0
- daita/plugins/__init__.py +141 -0
- daita/plugins/base.py +37 -0
- daita/plugins/base_db.py +167 -0
- daita/plugins/elasticsearch.py +849 -0
- daita/plugins/mcp.py +481 -0
- daita/plugins/mongodb.py +520 -0
- daita/plugins/mysql.py +362 -0
- daita/plugins/postgresql.py +342 -0
- daita/plugins/redis_messaging.py +500 -0
- daita/plugins/rest.py +537 -0
- daita/plugins/s3.py +770 -0
- daita/plugins/slack.py +729 -0
- daita/utils/__init__.py +18 -0
- daita_agents-0.2.0.dist-info/METADATA +409 -0
- daita_agents-0.2.0.dist-info/RECORD +69 -0
- daita_agents-0.2.0.dist-info/WHEEL +5 -0
- daita_agents-0.2.0.dist-info/entry_points.txt +2 -0
- daita_agents-0.2.0.dist-info/licenses/LICENSE +56 -0
- daita_agents-0.2.0.dist-info/top_level.txt +1 -0
daita/plugins/slack.py
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Slack plugin for Daita Agents.
|
|
3
|
+
|
|
4
|
+
Simple Slack messaging and collaboration - no over-engineering.
|
|
5
|
+
"""
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Dict, List, Optional, Union, TYPE_CHECKING
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from ..core.tools import AgentTool
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
class SlackPlugin:
|
|
19
|
+
"""
|
|
20
|
+
Simple Slack plugin for agents.
|
|
21
|
+
|
|
22
|
+
Handles Slack messaging, thread management, and file sharing with agent-specific features.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
token: str,
|
|
28
|
+
bot_user_oauth_token: Optional[str] = None,
|
|
29
|
+
app_token: Optional[str] = None,
|
|
30
|
+
default_channel: Optional[str] = None,
|
|
31
|
+
**kwargs
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Initialize Slack connection.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
token: Slack bot token (xoxb-...)
|
|
38
|
+
bot_user_oauth_token: Bot user OAuth token (optional, for extended permissions)
|
|
39
|
+
app_token: App-level token (optional, for Socket Mode)
|
|
40
|
+
default_channel: Default channel for messages (optional)
|
|
41
|
+
**kwargs: Additional Slack client parameters
|
|
42
|
+
"""
|
|
43
|
+
if not token or not token.strip():
|
|
44
|
+
raise ValueError("Slack token cannot be empty")
|
|
45
|
+
|
|
46
|
+
if not token.startswith(('xoxb-', 'xoxp-')):
|
|
47
|
+
raise ValueError("Invalid Slack token format. Expected bot token (xoxb-) or user token (xoxp-)")
|
|
48
|
+
|
|
49
|
+
self.token = token
|
|
50
|
+
self.bot_user_oauth_token = bot_user_oauth_token
|
|
51
|
+
self.app_token = app_token
|
|
52
|
+
self.default_channel = default_channel
|
|
53
|
+
|
|
54
|
+
# Store additional config
|
|
55
|
+
self.config = kwargs
|
|
56
|
+
|
|
57
|
+
self._client = None
|
|
58
|
+
self._user_info = None
|
|
59
|
+
self._channels_cache = {}
|
|
60
|
+
|
|
61
|
+
logger.debug(f"Slack plugin configured with token: {token[:12]}...")
|
|
62
|
+
|
|
63
|
+
async def connect(self):
|
|
64
|
+
"""Initialize Slack client and validate connection."""
|
|
65
|
+
if self._client is not None:
|
|
66
|
+
return # Already connected
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
from slack_sdk.web.async_client import AsyncWebClient
|
|
70
|
+
from slack_sdk.errors import SlackApiError
|
|
71
|
+
|
|
72
|
+
# Create Slack client
|
|
73
|
+
self._client = AsyncWebClient(token=self.token)
|
|
74
|
+
|
|
75
|
+
# Test connection and get bot info
|
|
76
|
+
try:
|
|
77
|
+
auth_response = await self._client.auth_test()
|
|
78
|
+
self._user_info = {
|
|
79
|
+
'user_id': auth_response.get('user_id'),
|
|
80
|
+
'team_id': auth_response.get('team_id'),
|
|
81
|
+
'team': auth_response.get('team'),
|
|
82
|
+
'user': auth_response.get('user'),
|
|
83
|
+
'bot_id': auth_response.get('bot_id')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logger.info(f"Connected to Slack as {self._user_info['user']} on team {self._user_info['team']}")
|
|
87
|
+
|
|
88
|
+
except SlackApiError as e:
|
|
89
|
+
if e.response['error'] == 'invalid_auth':
|
|
90
|
+
raise RuntimeError("Invalid Slack token. Please check your bot token.")
|
|
91
|
+
elif e.response['error'] == 'account_inactive':
|
|
92
|
+
raise RuntimeError("Slack account is inactive.")
|
|
93
|
+
else:
|
|
94
|
+
raise RuntimeError(f"Slack authentication failed: {e.response['error']}")
|
|
95
|
+
|
|
96
|
+
except ImportError:
|
|
97
|
+
raise RuntimeError("slack-sdk not installed. Run: pip install slack-sdk")
|
|
98
|
+
except Exception as e:
|
|
99
|
+
raise RuntimeError(f"Failed to connect to Slack: {e}")
|
|
100
|
+
|
|
101
|
+
async def disconnect(self):
|
|
102
|
+
"""Close Slack connection."""
|
|
103
|
+
if self._client:
|
|
104
|
+
# Slack SDK client doesn't need explicit closing
|
|
105
|
+
self._client = None
|
|
106
|
+
self._user_info = None
|
|
107
|
+
self._channels_cache = {}
|
|
108
|
+
logger.info("Disconnected from Slack")
|
|
109
|
+
|
|
110
|
+
async def send_message(
|
|
111
|
+
self,
|
|
112
|
+
channel: str,
|
|
113
|
+
text: Optional[str] = None,
|
|
114
|
+
blocks: Optional[List[Dict[str, Any]]] = None,
|
|
115
|
+
attachments: Optional[List[Dict[str, Any]]] = None,
|
|
116
|
+
thread_ts: Optional[str] = None,
|
|
117
|
+
reply_broadcast: bool = False,
|
|
118
|
+
**kwargs
|
|
119
|
+
) -> Dict[str, Any]:
|
|
120
|
+
"""
|
|
121
|
+
Send a message to a Slack channel.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
channel: Channel ID or name (#channel, @user)
|
|
125
|
+
text: Message text (required if no blocks)
|
|
126
|
+
blocks: Slack Block Kit blocks for rich formatting
|
|
127
|
+
attachments: Message attachments (legacy, use blocks instead)
|
|
128
|
+
thread_ts: Timestamp of parent message (for threaded replies)
|
|
129
|
+
reply_broadcast: Whether to broadcast thread reply to channel
|
|
130
|
+
**kwargs: Additional message parameters
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Message response with timestamp and metadata
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
result = await slack.send_message("#alerts", "System update complete")
|
|
137
|
+
"""
|
|
138
|
+
if self._client is None:
|
|
139
|
+
await self.connect()
|
|
140
|
+
|
|
141
|
+
# Use default channel if not specified
|
|
142
|
+
if not channel and self.default_channel:
|
|
143
|
+
channel = self.default_channel
|
|
144
|
+
|
|
145
|
+
if not channel:
|
|
146
|
+
raise ValueError("Channel must be specified or default_channel must be set")
|
|
147
|
+
|
|
148
|
+
# Validate message content
|
|
149
|
+
if not text and not blocks:
|
|
150
|
+
raise ValueError("Either text or blocks must be provided")
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
# Prepare message arguments
|
|
154
|
+
message_args = {
|
|
155
|
+
'channel': channel,
|
|
156
|
+
'text': text,
|
|
157
|
+
'thread_ts': thread_ts,
|
|
158
|
+
'reply_broadcast': reply_broadcast,
|
|
159
|
+
**kwargs
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Add blocks if provided
|
|
163
|
+
if blocks:
|
|
164
|
+
message_args['blocks'] = blocks
|
|
165
|
+
|
|
166
|
+
# Add attachments if provided (legacy support)
|
|
167
|
+
if attachments:
|
|
168
|
+
message_args['attachments'] = attachments
|
|
169
|
+
|
|
170
|
+
# Send message
|
|
171
|
+
response = await self._client.chat_postMessage(**message_args)
|
|
172
|
+
|
|
173
|
+
result = {
|
|
174
|
+
'ok': response['ok'],
|
|
175
|
+
'ts': response['ts'],
|
|
176
|
+
'channel': response['channel'],
|
|
177
|
+
'message': response.get('message', {}),
|
|
178
|
+
'thread_ts': thread_ts
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
logger.info(f"Sent message to {channel}: {text[:50] if text else 'blocks'}...")
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
logger.error(f"Failed to send Slack message: {e}")
|
|
186
|
+
raise RuntimeError(f"Slack send_message failed: {e}")
|
|
187
|
+
|
|
188
|
+
async def send_agent_summary(
|
|
189
|
+
self,
|
|
190
|
+
channel: str,
|
|
191
|
+
agent_results: Dict[str, Any],
|
|
192
|
+
title: Optional[str] = None,
|
|
193
|
+
thread_ts: Optional[str] = None
|
|
194
|
+
) -> Dict[str, Any]:
|
|
195
|
+
"""
|
|
196
|
+
Send a formatted summary of agent results to Slack.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
channel: Channel ID or name
|
|
200
|
+
agent_results: Agent execution results
|
|
201
|
+
title: Optional title for the summary
|
|
202
|
+
thread_ts: Optional thread timestamp
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Message response
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
result = await slack.send_agent_summary("#data-team", agent_results)
|
|
209
|
+
"""
|
|
210
|
+
if self._client is None:
|
|
211
|
+
await self.connect()
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
# Create formatted blocks for agent results
|
|
215
|
+
blocks = self._format_agent_results(agent_results, title)
|
|
216
|
+
|
|
217
|
+
# Send message with blocks
|
|
218
|
+
return await self.send_message(
|
|
219
|
+
channel=channel,
|
|
220
|
+
text=f"Agent Summary: {title or 'Results'}",
|
|
221
|
+
blocks=blocks,
|
|
222
|
+
thread_ts=thread_ts
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Failed to send agent summary: {e}")
|
|
227
|
+
raise RuntimeError(f"Slack send_agent_summary failed: {e}")
|
|
228
|
+
|
|
229
|
+
async def create_thread_from_workflow(
|
|
230
|
+
self,
|
|
231
|
+
channel: str,
|
|
232
|
+
workflow_results: Dict[str, Any],
|
|
233
|
+
title: Optional[str] = None
|
|
234
|
+
) -> Dict[str, Any]:
|
|
235
|
+
"""
|
|
236
|
+
Create a threaded discussion from workflow results.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
channel: Channel ID or name
|
|
240
|
+
workflow_results: Workflow execution results
|
|
241
|
+
title: Optional title for the thread
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Thread creation response with thread_ts
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
result = await slack.create_thread_from_workflow("#workflows", workflow_results)
|
|
248
|
+
"""
|
|
249
|
+
if self._client is None:
|
|
250
|
+
await self.connect()
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
# Create initial thread message
|
|
254
|
+
thread_title = title or f"Workflow Results - {datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
|
255
|
+
|
|
256
|
+
# Create main thread message
|
|
257
|
+
main_blocks = self._format_workflow_summary(workflow_results, thread_title)
|
|
258
|
+
|
|
259
|
+
main_response = await self.send_message(
|
|
260
|
+
channel=channel,
|
|
261
|
+
text=f"Workflow Thread: {thread_title}",
|
|
262
|
+
blocks=main_blocks
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
thread_ts = main_response['ts']
|
|
266
|
+
|
|
267
|
+
# Add individual agent results as thread replies
|
|
268
|
+
agents = workflow_results.get('agents', {})
|
|
269
|
+
for agent_id, agent_result in agents.items():
|
|
270
|
+
agent_blocks = self._format_agent_results(agent_result, f"Agent: {agent_id}")
|
|
271
|
+
|
|
272
|
+
await self.send_message(
|
|
273
|
+
channel=channel,
|
|
274
|
+
text=f"Agent {agent_id} Results",
|
|
275
|
+
blocks=agent_blocks,
|
|
276
|
+
thread_ts=thread_ts
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
result = {
|
|
280
|
+
'ok': True,
|
|
281
|
+
'thread_ts': thread_ts,
|
|
282
|
+
'channel': channel,
|
|
283
|
+
'agent_count': len(agents),
|
|
284
|
+
'main_message': main_response
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
logger.info(f"Created workflow thread in {channel} with {len(agents)} agents")
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.error(f"Failed to create workflow thread: {e}")
|
|
292
|
+
raise RuntimeError(f"Slack create_thread_from_workflow failed: {e}")
|
|
293
|
+
|
|
294
|
+
async def upload_file(
|
|
295
|
+
self,
|
|
296
|
+
channel: str,
|
|
297
|
+
file_path: str,
|
|
298
|
+
title: Optional[str] = None,
|
|
299
|
+
initial_comment: Optional[str] = None,
|
|
300
|
+
thread_ts: Optional[str] = None
|
|
301
|
+
) -> Dict[str, Any]:
|
|
302
|
+
"""
|
|
303
|
+
Upload a file to Slack.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
channel: Channel ID or name
|
|
307
|
+
file_path: Path to file to upload
|
|
308
|
+
title: File title (defaults to filename)
|
|
309
|
+
initial_comment: Comment to add with file
|
|
310
|
+
thread_ts: Thread timestamp (for threaded uploads)
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
File upload response
|
|
314
|
+
|
|
315
|
+
Example:
|
|
316
|
+
result = await slack.upload_file("#reports", "analysis.pdf", "Monthly Analysis")
|
|
317
|
+
"""
|
|
318
|
+
if self._client is None:
|
|
319
|
+
await self.connect()
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
if not os.path.exists(file_path):
|
|
323
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
324
|
+
|
|
325
|
+
# Get file info
|
|
326
|
+
file_name = os.path.basename(file_path)
|
|
327
|
+
file_title = title or file_name
|
|
328
|
+
|
|
329
|
+
# Upload file
|
|
330
|
+
response = await self._client.files_upload_v2(
|
|
331
|
+
channel=channel,
|
|
332
|
+
file=file_path,
|
|
333
|
+
title=file_title,
|
|
334
|
+
initial_comment=initial_comment,
|
|
335
|
+
thread_ts=thread_ts
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
result = {
|
|
339
|
+
'ok': response['ok'],
|
|
340
|
+
'file': response.get('file', {}),
|
|
341
|
+
'file_id': response.get('file', {}).get('id'),
|
|
342
|
+
'file_name': file_name,
|
|
343
|
+
'file_title': file_title,
|
|
344
|
+
'channel': channel,
|
|
345
|
+
'thread_ts': thread_ts
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
logger.info(f"Uploaded file {file_name} to {channel}")
|
|
349
|
+
return result
|
|
350
|
+
|
|
351
|
+
except Exception as e:
|
|
352
|
+
logger.error(f"Failed to upload file to Slack: {e}")
|
|
353
|
+
raise RuntimeError(f"Slack upload_file failed: {e}")
|
|
354
|
+
|
|
355
|
+
async def get_channel_history(
|
|
356
|
+
self,
|
|
357
|
+
channel: str,
|
|
358
|
+
limit: int = 100,
|
|
359
|
+
cursor: Optional[str] = None,
|
|
360
|
+
oldest: Optional[str] = None,
|
|
361
|
+
latest: Optional[str] = None
|
|
362
|
+
) -> List[Dict[str, Any]]:
|
|
363
|
+
"""
|
|
364
|
+
Get channel message history.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
channel: Channel ID or name
|
|
368
|
+
limit: Maximum number of messages to return
|
|
369
|
+
cursor: Pagination cursor
|
|
370
|
+
oldest: Oldest timestamp to include
|
|
371
|
+
latest: Latest timestamp to include
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
List of messages
|
|
375
|
+
|
|
376
|
+
Example:
|
|
377
|
+
messages = await slack.get_channel_history("#alerts", limit=50)
|
|
378
|
+
"""
|
|
379
|
+
if self._client is None:
|
|
380
|
+
await self.connect()
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
# Get conversation history
|
|
384
|
+
response = await self._client.conversations_history(
|
|
385
|
+
channel=channel,
|
|
386
|
+
limit=limit,
|
|
387
|
+
cursor=cursor,
|
|
388
|
+
oldest=oldest,
|
|
389
|
+
latest=latest
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
messages = response.get('messages', [])
|
|
393
|
+
|
|
394
|
+
# Format messages for easier processing
|
|
395
|
+
formatted_messages = []
|
|
396
|
+
for msg in messages:
|
|
397
|
+
formatted_msg = {
|
|
398
|
+
'ts': msg.get('ts'),
|
|
399
|
+
'user': msg.get('user'),
|
|
400
|
+
'text': msg.get('text', ''),
|
|
401
|
+
'type': msg.get('type'),
|
|
402
|
+
'subtype': msg.get('subtype'),
|
|
403
|
+
'thread_ts': msg.get('thread_ts'),
|
|
404
|
+
'reply_count': msg.get('reply_count', 0),
|
|
405
|
+
'blocks': msg.get('blocks', []),
|
|
406
|
+
'attachments': msg.get('attachments', [])
|
|
407
|
+
}
|
|
408
|
+
formatted_messages.append(formatted_msg)
|
|
409
|
+
|
|
410
|
+
logger.info(f"Retrieved {len(formatted_messages)} messages from {channel}")
|
|
411
|
+
return formatted_messages
|
|
412
|
+
|
|
413
|
+
except Exception as e:
|
|
414
|
+
logger.error(f"Failed to get channel history: {e}")
|
|
415
|
+
raise RuntimeError(f"Slack get_channel_history failed: {e}")
|
|
416
|
+
|
|
417
|
+
async def get_channels(self, types: str = "public_channel,private_channel") -> List[Dict[str, Any]]:
|
|
418
|
+
"""
|
|
419
|
+
Get list of channels the bot has access to.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
types: Channel types to include (comma-separated)
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
List of channel information
|
|
426
|
+
|
|
427
|
+
Example:
|
|
428
|
+
channels = await slack.get_channels()
|
|
429
|
+
"""
|
|
430
|
+
if self._client is None:
|
|
431
|
+
await self.connect()
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
# Get conversations list
|
|
435
|
+
response = await self._client.conversations_list(types=types)
|
|
436
|
+
|
|
437
|
+
channels = response.get('channels', [])
|
|
438
|
+
|
|
439
|
+
# Format channel info
|
|
440
|
+
formatted_channels = []
|
|
441
|
+
for channel in channels:
|
|
442
|
+
formatted_channel = {
|
|
443
|
+
'id': channel.get('id'),
|
|
444
|
+
'name': channel.get('name'),
|
|
445
|
+
'is_channel': channel.get('is_channel'),
|
|
446
|
+
'is_private': channel.get('is_private'),
|
|
447
|
+
'is_archived': channel.get('is_archived'),
|
|
448
|
+
'num_members': channel.get('num_members'),
|
|
449
|
+
'topic': channel.get('topic', {}).get('value', ''),
|
|
450
|
+
'purpose': channel.get('purpose', {}).get('value', '')
|
|
451
|
+
}
|
|
452
|
+
formatted_channels.append(formatted_channel)
|
|
453
|
+
|
|
454
|
+
# Update cache
|
|
455
|
+
self._channels_cache = {ch['name']: ch['id'] for ch in formatted_channels}
|
|
456
|
+
|
|
457
|
+
logger.info(f"Retrieved {len(formatted_channels)} channels")
|
|
458
|
+
return formatted_channels
|
|
459
|
+
|
|
460
|
+
except Exception as e:
|
|
461
|
+
logger.error(f"Failed to get channels: {e}")
|
|
462
|
+
raise RuntimeError(f"Slack get_channels failed: {e}")
|
|
463
|
+
|
|
464
|
+
def _format_agent_results(self, agent_results: Dict[str, Any], title: str) -> List[Dict[str, Any]]:
|
|
465
|
+
"""Format agent results as Slack Block Kit blocks."""
|
|
466
|
+
blocks = []
|
|
467
|
+
|
|
468
|
+
# Header block
|
|
469
|
+
blocks.append({
|
|
470
|
+
"type": "header",
|
|
471
|
+
"text": {
|
|
472
|
+
"type": "plain_text",
|
|
473
|
+
"text": title
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
# Agent status and timing
|
|
478
|
+
status = agent_results.get('status', 'unknown')
|
|
479
|
+
start_time = agent_results.get('start_time', 'N/A')
|
|
480
|
+
end_time = agent_results.get('end_time', 'N/A')
|
|
481
|
+
duration = agent_results.get('duration_ms', 0)
|
|
482
|
+
|
|
483
|
+
status_emoji = "" if status == "success" else "" if status == "error" else ""
|
|
484
|
+
|
|
485
|
+
blocks.append({
|
|
486
|
+
"type": "section",
|
|
487
|
+
"fields": [
|
|
488
|
+
{
|
|
489
|
+
"type": "mrkdwn",
|
|
490
|
+
"text": f"*Status:* {status_emoji} {status.title()}"
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"type": "mrkdwn",
|
|
494
|
+
"text": f"*Duration:* {duration:.1f}ms"
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
"type": "mrkdwn",
|
|
498
|
+
"text": f"*Started:* {start_time}"
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
"type": "mrkdwn",
|
|
502
|
+
"text": f"*Completed:* {end_time}"
|
|
503
|
+
}
|
|
504
|
+
]
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
# Results summary
|
|
508
|
+
if 'output' in agent_results:
|
|
509
|
+
output = agent_results['output']
|
|
510
|
+
if isinstance(output, dict):
|
|
511
|
+
output_text = json.dumps(output, indent=2)
|
|
512
|
+
else:
|
|
513
|
+
output_text = str(output)
|
|
514
|
+
|
|
515
|
+
# Truncate if too long
|
|
516
|
+
if len(output_text) > 2000:
|
|
517
|
+
output_text = output_text[:2000] + "..."
|
|
518
|
+
|
|
519
|
+
blocks.append({
|
|
520
|
+
"type": "section",
|
|
521
|
+
"text": {
|
|
522
|
+
"type": "mrkdwn",
|
|
523
|
+
"text": f"*Results:*\n```\n{output_text}\n```"
|
|
524
|
+
}
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
# Error information
|
|
528
|
+
if status == "error" and 'error' in agent_results:
|
|
529
|
+
blocks.append({
|
|
530
|
+
"type": "section",
|
|
531
|
+
"text": {
|
|
532
|
+
"type": "mrkdwn",
|
|
533
|
+
"text": f"*Error:*\n```\n{agent_results['error']}\n```"
|
|
534
|
+
}
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
return blocks
|
|
538
|
+
|
|
539
|
+
def _format_workflow_summary(self, workflow_results: Dict[str, Any], title: str) -> List[Dict[str, Any]]:
|
|
540
|
+
"""Format workflow results as Slack Block Kit blocks."""
|
|
541
|
+
blocks = []
|
|
542
|
+
|
|
543
|
+
# Header block
|
|
544
|
+
blocks.append({
|
|
545
|
+
"type": "header",
|
|
546
|
+
"text": {
|
|
547
|
+
"type": "plain_text",
|
|
548
|
+
"text": title
|
|
549
|
+
}
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
# Workflow summary
|
|
553
|
+
total_agents = len(workflow_results.get('agents', {}))
|
|
554
|
+
successful_agents = sum(1 for agent in workflow_results.get('agents', {}).values()
|
|
555
|
+
if agent.get('status') == 'success')
|
|
556
|
+
failed_agents = total_agents - successful_agents
|
|
557
|
+
|
|
558
|
+
workflow_status = " Success" if failed_agents == 0 else f" {failed_agents} Failed"
|
|
559
|
+
|
|
560
|
+
blocks.append({
|
|
561
|
+
"type": "section",
|
|
562
|
+
"fields": [
|
|
563
|
+
{
|
|
564
|
+
"type": "mrkdwn",
|
|
565
|
+
"text": f"*Status:* {workflow_status}"
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
"type": "mrkdwn",
|
|
569
|
+
"text": f"*Total Agents:* {total_agents}"
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"type": "mrkdwn",
|
|
573
|
+
"text": f"*Successful:* {successful_agents}"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"type": "mrkdwn",
|
|
577
|
+
"text": f"*Failed:* {failed_agents}"
|
|
578
|
+
}
|
|
579
|
+
]
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
# Add divider
|
|
583
|
+
blocks.append({"type": "divider"})
|
|
584
|
+
|
|
585
|
+
# Agent summary
|
|
586
|
+
if total_agents > 0:
|
|
587
|
+
blocks.append({
|
|
588
|
+
"type": "section",
|
|
589
|
+
"text": {
|
|
590
|
+
"type": "mrkdwn",
|
|
591
|
+
"text": "*Agent Results:*\nSee thread replies for detailed results from each agent."
|
|
592
|
+
}
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
return blocks
|
|
596
|
+
|
|
597
|
+
def get_tools(self) -> List['AgentTool']:
|
|
598
|
+
"""
|
|
599
|
+
Expose Slack operations as agent tools.
|
|
600
|
+
|
|
601
|
+
Returns:
|
|
602
|
+
List of AgentTool instances for Slack messaging operations
|
|
603
|
+
"""
|
|
604
|
+
from ..core.tools import AgentTool
|
|
605
|
+
|
|
606
|
+
return [
|
|
607
|
+
AgentTool(
|
|
608
|
+
name="send_slack_message",
|
|
609
|
+
description="Send a message to a Slack channel. Use for notifications, alerts, or sharing information with team channels.",
|
|
610
|
+
parameters={
|
|
611
|
+
"channel": {
|
|
612
|
+
"type": "string",
|
|
613
|
+
"description": "Channel name (e.g., #general, #alerts) or channel ID",
|
|
614
|
+
"required": True
|
|
615
|
+
},
|
|
616
|
+
"text": {
|
|
617
|
+
"type": "string",
|
|
618
|
+
"description": "Message text to send",
|
|
619
|
+
"required": True
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
handler=self._tool_send_message,
|
|
623
|
+
category="communication",
|
|
624
|
+
source="plugin",
|
|
625
|
+
plugin_name="Slack",
|
|
626
|
+
timeout_seconds=30
|
|
627
|
+
),
|
|
628
|
+
AgentTool(
|
|
629
|
+
name="send_slack_summary",
|
|
630
|
+
description="Send a formatted agent summary to Slack with results and metadata. Use for reporting agent execution results.",
|
|
631
|
+
parameters={
|
|
632
|
+
"channel": {
|
|
633
|
+
"type": "string",
|
|
634
|
+
"description": "Channel name or ID to send summary to",
|
|
635
|
+
"required": True
|
|
636
|
+
},
|
|
637
|
+
"summary": {
|
|
638
|
+
"type": "string",
|
|
639
|
+
"description": "Summary text describing the results",
|
|
640
|
+
"required": True
|
|
641
|
+
},
|
|
642
|
+
"results": {
|
|
643
|
+
"type": "object",
|
|
644
|
+
"description": "Results data to include in the summary",
|
|
645
|
+
"required": False
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
handler=self._tool_send_summary,
|
|
649
|
+
category="communication",
|
|
650
|
+
source="plugin",
|
|
651
|
+
plugin_name="Slack",
|
|
652
|
+
timeout_seconds=30
|
|
653
|
+
),
|
|
654
|
+
AgentTool(
|
|
655
|
+
name="list_slack_channels",
|
|
656
|
+
description="List all Slack channels the bot has access to",
|
|
657
|
+
parameters={},
|
|
658
|
+
handler=self._tool_list_channels,
|
|
659
|
+
category="communication",
|
|
660
|
+
source="plugin",
|
|
661
|
+
plugin_name="Slack",
|
|
662
|
+
timeout_seconds=30
|
|
663
|
+
)
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
async def _tool_send_message(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
667
|
+
"""Tool handler for send_slack_message"""
|
|
668
|
+
channel = args.get("channel")
|
|
669
|
+
text = args.get("text")
|
|
670
|
+
|
|
671
|
+
result = await self.send_message(channel, text)
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
"success": True,
|
|
675
|
+
"channel": result.get("channel"),
|
|
676
|
+
"timestamp": result.get("ts"),
|
|
677
|
+
"message_sent": True
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
async def _tool_send_summary(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
681
|
+
"""Tool handler for send_slack_summary"""
|
|
682
|
+
channel = args.get("channel")
|
|
683
|
+
summary = args.get("summary")
|
|
684
|
+
results = args.get("results", {})
|
|
685
|
+
|
|
686
|
+
await self.send_agent_summary(
|
|
687
|
+
channel=channel,
|
|
688
|
+
agent_results={"summary": summary, "data": results}
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
return {
|
|
692
|
+
"success": True,
|
|
693
|
+
"channel": channel,
|
|
694
|
+
"summary_sent": True
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async def _tool_list_channels(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
698
|
+
"""Tool handler for list_slack_channels"""
|
|
699
|
+
channels = await self.get_channels()
|
|
700
|
+
|
|
701
|
+
# Simplify channel data for LLM
|
|
702
|
+
simplified = [
|
|
703
|
+
{
|
|
704
|
+
"name": ch["name"],
|
|
705
|
+
"id": ch["id"],
|
|
706
|
+
"is_private": ch["is_private"],
|
|
707
|
+
"members": ch.get("num_members", 0)
|
|
708
|
+
}
|
|
709
|
+
for ch in channels
|
|
710
|
+
]
|
|
711
|
+
|
|
712
|
+
return {
|
|
713
|
+
"success": True,
|
|
714
|
+
"channels": simplified,
|
|
715
|
+
"count": len(simplified)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
# Context manager support
|
|
719
|
+
async def __aenter__(self):
|
|
720
|
+
await self.connect()
|
|
721
|
+
return self
|
|
722
|
+
|
|
723
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
724
|
+
await self.disconnect()
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def slack(**kwargs) -> SlackPlugin:
|
|
728
|
+
"""Create Slack plugin with simplified interface."""
|
|
729
|
+
return SlackPlugin(**kwargs)
|