slide-space-monkey 0.1.1__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.
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slide-space-monkey
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A simple, powerful way to deploy Tyler AI agents as Slack bots
|
|
5
|
+
Project-URL: Homepage, https://github.com/adamwdraper/slide
|
|
6
|
+
Project-URL: Repository, https://github.com/adamwdraper/slide
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/adamwdraper/slide/issues
|
|
8
|
+
Author: adamwdraper
|
|
9
|
+
License: MIT
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Communications :: Chat
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: fastapi>=0.104.0
|
|
21
|
+
Requires-Dist: pydantic>=2.10.0
|
|
22
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
23
|
+
Requires-Dist: requests>=2.32.0
|
|
24
|
+
Requires-Dist: slack-bolt>=1.18.0
|
|
25
|
+
Requires-Dist: slide-narrator>=0.2.0
|
|
26
|
+
Requires-Dist: slide-tyler>=1.1.0
|
|
27
|
+
Requires-Dist: uvicorn[standard]>=0.24.0
|
|
28
|
+
Requires-Dist: weave>=0.51.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: coverage>=7.6.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.25.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# Space Monkey - Tyler Slack Bot
|
|
37
|
+
|
|
38
|
+
A simple, powerful way to deploy Tyler AI agents as Slack bots with just a few lines of code.
|
|
39
|
+
|
|
40
|
+
## Overview
|
|
41
|
+
|
|
42
|
+
Space Monkey provides a clean, class-based API for creating and deploying Tyler agents as Slack bots. It handles all the complexity of Slack integration, message routing, thread management, and storage while exposing a simple interface for agent configuration.
|
|
43
|
+
|
|
44
|
+
## Features
|
|
45
|
+
|
|
46
|
+
- **Simple API**: Just import `SlackApp`, `Agent`, and stores from one package
|
|
47
|
+
- **Intelligent Message Routing**: Automatically classifies messages and routes appropriately
|
|
48
|
+
- **Thread Management**: Persistent conversation threads with configurable storage backends
|
|
49
|
+
- **File Handling**: Built-in support for file attachments and processing
|
|
50
|
+
- **Health Monitoring**: Optional health check endpoints for production deployments
|
|
51
|
+
- **Weave Integration**: Built-in tracing and monitoring support
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
### 1. Install the Package
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install slide-space-monkey
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 2. Set Up Environment Variables
|
|
62
|
+
|
|
63
|
+
Create a `.env` file:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Required: Slack Configuration
|
|
67
|
+
SLACK_BOT_TOKEN=xoxb-your-bot-token
|
|
68
|
+
SLACK_APP_TOKEN=xapp-your-app-token
|
|
69
|
+
|
|
70
|
+
# Optional: Weave Monitoring
|
|
71
|
+
WANDB_API_KEY=your-wandb-key
|
|
72
|
+
WANDB_PROJECT=your-project-name
|
|
73
|
+
|
|
74
|
+
# Optional: Health Check
|
|
75
|
+
HEALTH_CHECK_URL=http://healthcheck:8000/ping-receiver
|
|
76
|
+
|
|
77
|
+
# Optional: Database (defaults to in-memory)
|
|
78
|
+
NARRATOR_DB_TYPE=postgresql
|
|
79
|
+
NARRATOR_DB_USER=tyler
|
|
80
|
+
NARRATOR_DB_PASSWORD=password
|
|
81
|
+
NARRATOR_DB_HOST=localhost
|
|
82
|
+
NARRATOR_DB_PORT=5432
|
|
83
|
+
NARRATOR_DB_NAME=tyler
|
|
84
|
+
|
|
85
|
+
# Optional: File Storage (defaults to ~/.tyler/files)
|
|
86
|
+
NARRATOR_FILE_STORAGE_PATH=/data/files
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 3. Create Your Bot
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
import asyncio
|
|
93
|
+
from space_monkey import SlackApp, ThreadStore, FileStore, Agent
|
|
94
|
+
|
|
95
|
+
async def main():
|
|
96
|
+
# Create stores
|
|
97
|
+
thread_store = await ThreadStore.create() # In-memory by default
|
|
98
|
+
file_store = await FileStore.create() # Default file storage
|
|
99
|
+
|
|
100
|
+
# Create your agent
|
|
101
|
+
agent = Agent(
|
|
102
|
+
name="SlackBot",
|
|
103
|
+
model_name="gpt-4.1",
|
|
104
|
+
purpose="To be a helpful assistant in Slack",
|
|
105
|
+
tools=["web", "files"],
|
|
106
|
+
temperature=0.7
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Start the Slack app
|
|
110
|
+
app = SlackApp(
|
|
111
|
+
agent=agent,
|
|
112
|
+
thread_store=thread_store,
|
|
113
|
+
file_store=file_store
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
await app.start()
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
asyncio.run(main())
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
That's it! Your Tyler agent is now running as a Slack bot.
|
|
123
|
+
|
|
124
|
+
## Advanced Configuration
|
|
125
|
+
|
|
126
|
+
### Database Storage
|
|
127
|
+
|
|
128
|
+
For production deployments, use a persistent database:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from space_monkey import ThreadStore, FileStore
|
|
132
|
+
|
|
133
|
+
# PostgreSQL
|
|
134
|
+
thread_store = await ThreadStore.create(
|
|
135
|
+
"postgresql+asyncpg://user:pass@localhost/db"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# SQLite
|
|
139
|
+
thread_store = await ThreadStore.create(
|
|
140
|
+
"sqlite+aiosqlite:///path/to/db.sqlite"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Custom file storage
|
|
144
|
+
file_store = await FileStore.create(
|
|
145
|
+
base_path="/data/files",
|
|
146
|
+
max_file_size=100 * 1024 * 1024, # 100MB
|
|
147
|
+
max_storage_size=10 * 1024 * 1024 * 1024 # 10GB
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Custom Agent Configuration
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# Create a custom agent with specific tools and settings
|
|
155
|
+
agent = Agent(
|
|
156
|
+
name="CustomBot",
|
|
157
|
+
model_name="gpt-4.1",
|
|
158
|
+
purpose="Your custom agent purpose",
|
|
159
|
+
tools=["notion:notion-search", "web", "files"],
|
|
160
|
+
temperature=0.7,
|
|
161
|
+
version="1.0.0"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Tyler Agent with custom settings
|
|
165
|
+
agent = Agent(
|
|
166
|
+
name="CustomBot",
|
|
167
|
+
model_name="gpt-4.1",
|
|
168
|
+
purpose="Your custom agent purpose",
|
|
169
|
+
tools=["notion:notion-search"],
|
|
170
|
+
temperature=0.5
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### App Configuration
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
app = SlackApp(
|
|
178
|
+
agent=agent,
|
|
179
|
+
thread_store=thread_store,
|
|
180
|
+
file_store=file_store,
|
|
181
|
+
health_check_url="http://healthcheck:8000/ping-receiver",
|
|
182
|
+
weave_project="my-slack-bot"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Start with custom host/port
|
|
186
|
+
await app.start(host="0.0.0.0", port=8080)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Agent Configuration
|
|
190
|
+
|
|
191
|
+
Space Monkey uses Tyler Agent directly, giving you full access to all Tyler's capabilities:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
agent = Agent(
|
|
195
|
+
name="MyBot", # Agent name
|
|
196
|
+
model_name="gpt-4.1", # Model to use
|
|
197
|
+
purpose="Your agent's purpose and instructions",
|
|
198
|
+
tools=["web", "files"], # Available tools
|
|
199
|
+
temperature=0.7, # Model temperature
|
|
200
|
+
version="1.0.0" # Agent version
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
You can configure agents for any use case by adjusting the purpose and tools:
|
|
205
|
+
- HR/People assistance with Notion integration
|
|
206
|
+
- Customer support with web search
|
|
207
|
+
- Technical assistance with file processing
|
|
208
|
+
- And any other conversational AI use case
|
|
209
|
+
|
|
210
|
+
## Message Routing
|
|
211
|
+
|
|
212
|
+
Tyler Slack Bot automatically handles intelligent message routing:
|
|
213
|
+
|
|
214
|
+
1. **Direct Messages**: All DM messages are processed by your agent
|
|
215
|
+
2. **@Mentions**: Messages that mention the bot are always processed
|
|
216
|
+
3. **Channel Messages**: Automatically classified to determine if they need a response
|
|
217
|
+
4. **Thread Replies**: Continues conversations in threads appropriately
|
|
218
|
+
5. **Reactions**: Simple acknowledgments get emoji reactions instead of text responses
|
|
219
|
+
|
|
220
|
+
This happens automatically - you just define your agent's behavior and the bot handles the rest.
|
|
221
|
+
|
|
222
|
+
## Storage Backends
|
|
223
|
+
|
|
224
|
+
### Thread Storage
|
|
225
|
+
|
|
226
|
+
- **In-Memory**: Fast, ephemeral (default for development)
|
|
227
|
+
- **SQLite**: Local persistence (good for single-instance deployments)
|
|
228
|
+
- **PostgreSQL**: Production-ready with full ACID compliance
|
|
229
|
+
|
|
230
|
+
### File Storage
|
|
231
|
+
|
|
232
|
+
- **Local File System**: Sharded directory structure for efficient file organization
|
|
233
|
+
- **Configurable Limits**: Set maximum file sizes and total storage limits
|
|
234
|
+
- **MIME Type Validation**: Automatic file type detection and validation
|
|
235
|
+
|
|
236
|
+
## Production Deployment
|
|
237
|
+
|
|
238
|
+
### Docker
|
|
239
|
+
|
|
240
|
+
```dockerfile
|
|
241
|
+
FROM python:3.11-slim
|
|
242
|
+
|
|
243
|
+
WORKDIR /app
|
|
244
|
+
COPY requirements.txt .
|
|
245
|
+
RUN pip install -r requirements.txt
|
|
246
|
+
|
|
247
|
+
COPY . .
|
|
248
|
+
CMD ["python", "bot.py"]
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Environment Configuration
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# Production .env
|
|
255
|
+
SLACK_BOT_TOKEN=xoxb-prod-token
|
|
256
|
+
SLACK_APP_TOKEN=xapp-prod-token
|
|
257
|
+
|
|
258
|
+
# Database
|
|
259
|
+
TYLER_DB_TYPE=postgresql
|
|
260
|
+
TYLER_DB_USER=tyler_prod
|
|
261
|
+
TYLER_DB_PASSWORD=secure_password
|
|
262
|
+
TYLER_DB_HOST=db.example.com
|
|
263
|
+
TYLER_DB_PORT=5432
|
|
264
|
+
TYLER_DB_NAME=tyler_prod
|
|
265
|
+
|
|
266
|
+
# File Storage
|
|
267
|
+
TYLER_FILE_STORAGE_PATH=/data/files
|
|
268
|
+
|
|
269
|
+
# Monitoring
|
|
270
|
+
WANDB_API_KEY=prod-key
|
|
271
|
+
WANDB_PROJECT=slack-bot-prod
|
|
272
|
+
HEALTH_CHECK_URL=http://healthcheck:8000/ping-receiver
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Health Monitoring
|
|
276
|
+
|
|
277
|
+
The bot includes built-in health check endpoints and optional external health monitoring:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
app = SlackApp(
|
|
281
|
+
agent=agent,
|
|
282
|
+
thread_store=thread_store,
|
|
283
|
+
file_store=file_store,
|
|
284
|
+
health_check_url="http://healthcheck:8000/ping-receiver"
|
|
285
|
+
)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## API Reference
|
|
289
|
+
|
|
290
|
+
### SlackApp
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
class SlackApp:
|
|
294
|
+
def __init__(
|
|
295
|
+
self,
|
|
296
|
+
agent: Agent,
|
|
297
|
+
thread_store: ThreadStore,
|
|
298
|
+
file_store: FileStore,
|
|
299
|
+
health_check_url: Optional[str] = None,
|
|
300
|
+
weave_project: Optional[str] = None
|
|
301
|
+
):
|
|
302
|
+
"""Initialize Slack app with agent and stores."""
|
|
303
|
+
|
|
304
|
+
async def start(
|
|
305
|
+
self,
|
|
306
|
+
host: str = "0.0.0.0",
|
|
307
|
+
port: int = 8000
|
|
308
|
+
) -> None:
|
|
309
|
+
"""Start the Slack bot app."""
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Agent
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
class Agent:
|
|
316
|
+
def __init__(
|
|
317
|
+
self,
|
|
318
|
+
name: str,
|
|
319
|
+
model_name: str,
|
|
320
|
+
purpose: str,
|
|
321
|
+
tools: Optional[List[str]] = None,
|
|
322
|
+
temperature: Optional[float] = None,
|
|
323
|
+
version: str = "1.0.0",
|
|
324
|
+
thread_store: Optional[ThreadStore] = None,
|
|
325
|
+
file_store: Optional[FileStore] = None
|
|
326
|
+
):
|
|
327
|
+
"""Create a Tyler agent with full configuration options."""
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Stores
|
|
331
|
+
|
|
332
|
+
```python
|
|
333
|
+
# ThreadStore
|
|
334
|
+
thread_store = await ThreadStore.create() # In-memory
|
|
335
|
+
thread_store = await ThreadStore.create(database_url) # Database
|
|
336
|
+
|
|
337
|
+
# FileStore
|
|
338
|
+
file_store = await FileStore.create() # Default settings
|
|
339
|
+
file_store = await FileStore.create(
|
|
340
|
+
base_path="/path/to/files",
|
|
341
|
+
max_file_size=100 * 1024 * 1024,
|
|
342
|
+
max_storage_size=10 * 1024 * 1024 * 1024
|
|
343
|
+
)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Examples
|
|
347
|
+
|
|
348
|
+
### Simple HR Bot
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
import asyncio
|
|
352
|
+
from space_monkey import SlackApp, Agent, ThreadStore, FileStore
|
|
353
|
+
|
|
354
|
+
async def main():
|
|
355
|
+
# Simple setup for an HR assistant
|
|
356
|
+
thread_store = await ThreadStore.create()
|
|
357
|
+
file_store = await FileStore.create()
|
|
358
|
+
|
|
359
|
+
agent = Agent(
|
|
360
|
+
name="HRBot",
|
|
361
|
+
model_name="gpt-4.1",
|
|
362
|
+
purpose="To help with HR and people team questions",
|
|
363
|
+
tools=["notion:notion-search"],
|
|
364
|
+
temperature=0.7
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
app = SlackApp(
|
|
368
|
+
agent=agent,
|
|
369
|
+
thread_store=thread_store,
|
|
370
|
+
file_store=file_store
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
await app.start()
|
|
374
|
+
|
|
375
|
+
asyncio.run(main())
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Custom Agent with Database
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
import asyncio
|
|
382
|
+
from space_monkey import SlackApp, Agent, ThreadStore, FileStore
|
|
383
|
+
|
|
384
|
+
async def main():
|
|
385
|
+
# Production setup with PostgreSQL
|
|
386
|
+
thread_store = await ThreadStore.create(
|
|
387
|
+
"postgresql+asyncpg://user:pass@localhost/db"
|
|
388
|
+
)
|
|
389
|
+
file_store = await FileStore.create(base_path="/data/files")
|
|
390
|
+
|
|
391
|
+
# Custom agent
|
|
392
|
+
agent = Agent(
|
|
393
|
+
name="SupportBot",
|
|
394
|
+
model_name="gpt-4.1",
|
|
395
|
+
purpose="Help users with technical support questions",
|
|
396
|
+
tools=["web", "files"],
|
|
397
|
+
temperature=0.3
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
app = SlackApp(
|
|
401
|
+
agent=agent,
|
|
402
|
+
thread_store=thread_store,
|
|
403
|
+
file_store=file_store,
|
|
404
|
+
weave_project="support-bot"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
await app.start(port=8080)
|
|
408
|
+
|
|
409
|
+
asyncio.run(main())
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## Contributing
|
|
413
|
+
|
|
414
|
+
Tyler Slack Bot is part of the Tyler ecosystem. See the main Tyler documentation for information about contributing to the project.
|
|
415
|
+
|
|
416
|
+
## License
|
|
417
|
+
|
|
418
|
+
MIT License - see LICENSE file for details.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
space_monkey/__init__.py,sha256=U0KBTXnV9ITv_kRCEUydQNOHeBeJBq41Jvknh9UAY88,407
|
|
2
|
+
space_monkey/classifier.py,sha256=qJiW0cllmJdb9UvQi68cwJOEhhSJ3OeD52N6aCG8JQ8,1208
|
|
3
|
+
space_monkey/slack_app.py,sha256=Uz0cQ1Xr8u1um_Y0QxTctR7rrS9Bn5T_t-fwh0AnO9U,23366
|
|
4
|
+
slide_space_monkey-0.1.1.dist-info/METADATA,sha256=xe7DjQnyGO2igBSV6BZ1HZmGn7yNwGaW6kBokgqQ59M,10830
|
|
5
|
+
slide_space_monkey-0.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
6
|
+
slide_space_monkey-0.1.1.dist-info/RECORD,,
|
space_monkey/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Space Monkey - Tyler Slack Bot
|
|
3
|
+
|
|
4
|
+
A simple, powerful way to deploy Tyler AI agents as Slack bots.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Re-export core components from other packages
|
|
8
|
+
from narrator import ThreadStore, FileStore
|
|
9
|
+
from tyler import Agent
|
|
10
|
+
|
|
11
|
+
# Import our own classes
|
|
12
|
+
from .slack_app import SlackApp
|
|
13
|
+
|
|
14
|
+
# Version
|
|
15
|
+
__version__ = "0.1.1"
|
|
16
|
+
|
|
17
|
+
# Main exports
|
|
18
|
+
__all__ = [
|
|
19
|
+
"SlackApp",
|
|
20
|
+
"Agent",
|
|
21
|
+
"ThreadStore",
|
|
22
|
+
"FileStore"
|
|
23
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message classifier for Space Monkey internal use
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
import weave
|
|
6
|
+
from tyler import Agent
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
@weave.op()
|
|
11
|
+
def initialize_message_classifier_agent(thread_store=None, file_store=None, model_name="gpt-4.1", purpose_prompt=None, bot_user_id=None):
|
|
12
|
+
"""
|
|
13
|
+
Initialize and return a classifier agent that determines if/how the main agent should respond.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
thread_store: The thread store instance for the agent to use (optional)
|
|
17
|
+
file_store: The file store instance for the agent to use (optional)
|
|
18
|
+
model_name: The model to use for the agent (default: gpt-4.1)
|
|
19
|
+
purpose_prompt: The purpose prompt for the agent
|
|
20
|
+
bot_user_id: The Slack user ID of the bot (optional)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Agent: An initialized message classifier agent
|
|
24
|
+
"""
|
|
25
|
+
message_classifier_agent = Agent(
|
|
26
|
+
name="MessageClassifier",
|
|
27
|
+
model_name=model_name,
|
|
28
|
+
version="2.0.0",
|
|
29
|
+
purpose=purpose_prompt.format(bot_user_id=bot_user_id),
|
|
30
|
+
thread_store=thread_store,
|
|
31
|
+
file_store=file_store
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
logger.info("MessageClassifier agent initialized")
|
|
35
|
+
return message_classifier_agent
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SlackApp class for Slack App integration
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import copy
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
import threading
|
|
11
|
+
import requests
|
|
12
|
+
import weave
|
|
13
|
+
from contextlib import asynccontextmanager
|
|
14
|
+
from dotenv import load_dotenv
|
|
15
|
+
from slack_bolt.async_app import AsyncApp
|
|
16
|
+
from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler
|
|
17
|
+
from fastapi import FastAPI
|
|
18
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
19
|
+
import uvicorn
|
|
20
|
+
|
|
21
|
+
from narrator import Thread, Message
|
|
22
|
+
from tyler.tools.slack import generate_slack_blocks
|
|
23
|
+
|
|
24
|
+
# Set up logging
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
class SlackApp:
|
|
28
|
+
"""
|
|
29
|
+
Main SlackApp class that encapsulates all Slack integration logic.
|
|
30
|
+
|
|
31
|
+
This class provides a clean interface for running Tyler agents as Slack bots
|
|
32
|
+
with intelligent message routing, thread management, and health monitoring.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
agent,
|
|
38
|
+
thread_store,
|
|
39
|
+
file_store,
|
|
40
|
+
health_check_url: str = None,
|
|
41
|
+
weave_project: str = None
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Initialize SlackApp with agent and stores.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
agent: The main Tyler agent to handle conversations
|
|
48
|
+
thread_store: ThreadStore instance for conversation persistence
|
|
49
|
+
file_store: FileStore instance for file handling
|
|
50
|
+
health_check_url: Optional URL for health check pings
|
|
51
|
+
weave_project: Optional Weave project name for tracing
|
|
52
|
+
"""
|
|
53
|
+
# Load environment variables
|
|
54
|
+
load_dotenv()
|
|
55
|
+
|
|
56
|
+
# Store configuration
|
|
57
|
+
self.agent = agent
|
|
58
|
+
self.thread_store = thread_store
|
|
59
|
+
self.file_store = file_store
|
|
60
|
+
self.health_check_url = health_check_url or os.getenv("HEALTH_CHECK_URL")
|
|
61
|
+
self.weave_project = weave_project or os.getenv("WANDB_PROJECT")
|
|
62
|
+
|
|
63
|
+
# Internal state
|
|
64
|
+
self.slack_app = None
|
|
65
|
+
self.socket_handler = None
|
|
66
|
+
self.bot_user_id = None
|
|
67
|
+
self.message_classifier_agent = None
|
|
68
|
+
self.health_thread = None
|
|
69
|
+
|
|
70
|
+
# FastAPI app for server functionality
|
|
71
|
+
self.fastapi_app = FastAPI(
|
|
72
|
+
title="Space Monkey Slack Agent",
|
|
73
|
+
lifespan=self._lifespan
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Add CORS middleware
|
|
77
|
+
self.fastapi_app.add_middleware(
|
|
78
|
+
CORSMiddleware,
|
|
79
|
+
allow_origins=["*"],
|
|
80
|
+
allow_credentials=True,
|
|
81
|
+
allow_methods=["*"],
|
|
82
|
+
allow_headers=["*"],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@asynccontextmanager
|
|
86
|
+
async def _lifespan(self, app: FastAPI):
|
|
87
|
+
"""FastAPI lifespan context manager for startup/shutdown logic."""
|
|
88
|
+
# Startup
|
|
89
|
+
await self._startup()
|
|
90
|
+
yield
|
|
91
|
+
# Shutdown
|
|
92
|
+
await self._shutdown()
|
|
93
|
+
|
|
94
|
+
async def _startup(self):
|
|
95
|
+
"""Initialize all components during startup."""
|
|
96
|
+
logger.info("Starting Space Monkey Slack Agent...")
|
|
97
|
+
|
|
98
|
+
# Initialize Weave if configured
|
|
99
|
+
await self._init_weave()
|
|
100
|
+
|
|
101
|
+
# Initialize Slack app and get bot user ID
|
|
102
|
+
await self._init_slack_app()
|
|
103
|
+
|
|
104
|
+
# Initialize message classifier
|
|
105
|
+
await self._init_classifier()
|
|
106
|
+
|
|
107
|
+
# Register event handlers
|
|
108
|
+
self._register_event_handlers()
|
|
109
|
+
|
|
110
|
+
# Start Slack socket connection
|
|
111
|
+
await self._start_slack()
|
|
112
|
+
|
|
113
|
+
# Start health monitoring
|
|
114
|
+
self._start_health_monitoring()
|
|
115
|
+
|
|
116
|
+
logger.info("Space Monkey Slack Agent started successfully")
|
|
117
|
+
|
|
118
|
+
async def _shutdown(self):
|
|
119
|
+
"""Clean shutdown logic."""
|
|
120
|
+
logger.info("Shutting down Space Monkey Slack Agent...")
|
|
121
|
+
|
|
122
|
+
if self.socket_handler:
|
|
123
|
+
await self.socket_handler.close_async()
|
|
124
|
+
|
|
125
|
+
# Close database connections if available
|
|
126
|
+
if (self.thread_store and
|
|
127
|
+
hasattr(self.thread_store, '_backend') and
|
|
128
|
+
hasattr(self.thread_store._backend, 'engine')):
|
|
129
|
+
try:
|
|
130
|
+
await self.thread_store._backend.engine.dispose()
|
|
131
|
+
logger.info("Database connections closed")
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"Error closing database connections: {e}")
|
|
134
|
+
|
|
135
|
+
async def _init_weave(self):
|
|
136
|
+
"""Initialize Weave monitoring if configured."""
|
|
137
|
+
try:
|
|
138
|
+
if os.getenv("WANDB_API_KEY") and self.weave_project:
|
|
139
|
+
weave.init(self.weave_project)
|
|
140
|
+
logger.info("Weave tracing initialized successfully")
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning(f"Failed to initialize weave tracing: {e}")
|
|
143
|
+
|
|
144
|
+
async def _init_slack_app(self):
|
|
145
|
+
"""Initialize the Slack app and get bot user ID."""
|
|
146
|
+
# Create Slack app
|
|
147
|
+
self.slack_app = AsyncApp(token=os.environ["SLACK_BOT_TOKEN"])
|
|
148
|
+
|
|
149
|
+
# Get bot user ID for mention detection
|
|
150
|
+
auth_response = await self.slack_app.client.auth_test()
|
|
151
|
+
self.bot_user_id = auth_response["user_id"]
|
|
152
|
+
logger.info(f"Agent initialized with user ID: {self.bot_user_id}")
|
|
153
|
+
|
|
154
|
+
async def _init_classifier(self):
|
|
155
|
+
"""Initialize the message classifier agent."""
|
|
156
|
+
# Import here to avoid circular imports
|
|
157
|
+
from .classifier import initialize_message_classifier_agent
|
|
158
|
+
|
|
159
|
+
# Get classifier prompt version from environment
|
|
160
|
+
classifier_prompt_version = os.getenv("MESSAGE_CLASSIFIER_PROMPT_VERSION")
|
|
161
|
+
if not classifier_prompt_version:
|
|
162
|
+
logger.error("MESSAGE_CLASSIFIER_PROMPT_VERSION not set")
|
|
163
|
+
raise RuntimeError("MESSAGE_CLASSIFIER_PROMPT_VERSION not set")
|
|
164
|
+
|
|
165
|
+
# Fetch prompt from Weave
|
|
166
|
+
try:
|
|
167
|
+
classifier_prompt_obj = weave.ref(f"MessageClassifierPurposePrompt:{classifier_prompt_version}").get()
|
|
168
|
+
logger.info(f"Fetched classifier prompt: {classifier_prompt_version}")
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Failed to load classifier prompt: {e}")
|
|
171
|
+
raise RuntimeError(f"Failed to load classifier prompt: {e}")
|
|
172
|
+
|
|
173
|
+
# Initialize classifier agent
|
|
174
|
+
self.message_classifier_agent = initialize_message_classifier_agent(
|
|
175
|
+
model_name="gpt-4.1",
|
|
176
|
+
purpose_prompt=classifier_prompt_obj,
|
|
177
|
+
thread_store=self.thread_store,
|
|
178
|
+
file_store=self.file_store,
|
|
179
|
+
bot_user_id=self.bot_user_id
|
|
180
|
+
)
|
|
181
|
+
logger.info("Message classifier agent initialized")
|
|
182
|
+
|
|
183
|
+
def _register_event_handlers(self):
|
|
184
|
+
"""Register Slack event handlers."""
|
|
185
|
+
# Global middleware for logging
|
|
186
|
+
@self.slack_app.use
|
|
187
|
+
async def log_all_events(client, context, logger, payload, next):
|
|
188
|
+
try:
|
|
189
|
+
logger.info(f"MIDDLEWARE: Received payload: {list(payload.keys())}")
|
|
190
|
+
if isinstance(payload, dict) and "type" in payload:
|
|
191
|
+
event_type = payload["type"]
|
|
192
|
+
logger.critical(f"MIDDLEWARE: Event type '{event_type}'")
|
|
193
|
+
|
|
194
|
+
if event_type in ["reaction_added", "reaction_removed"]:
|
|
195
|
+
logger.critical(f"MIDDLEWARE: REACTION EVENT: {json.dumps(payload)}")
|
|
196
|
+
elif event_type == "message":
|
|
197
|
+
logger.critical(f"MIDDLEWARE: MESSAGE EVENT: channel={payload.get('channel')}, user={payload.get('user')}, ts={payload.get('ts')}")
|
|
198
|
+
|
|
199
|
+
await next()
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.error(f"Error in middleware: {str(e)}")
|
|
202
|
+
await next()
|
|
203
|
+
|
|
204
|
+
# Register event handlers
|
|
205
|
+
self.slack_app.event({"type": "message", "subtype": None})(self._handle_user_message)
|
|
206
|
+
self.slack_app.event("app_mention")(self._handle_app_mention)
|
|
207
|
+
self.slack_app.event("reaction_added")(self._handle_reaction_added)
|
|
208
|
+
self.slack_app.event("reaction_removed")(self._handle_reaction_removed)
|
|
209
|
+
|
|
210
|
+
async def _start_slack(self):
|
|
211
|
+
"""Start the Slack socket connection."""
|
|
212
|
+
self.socket_handler = AsyncSocketModeHandler(self.slack_app, os.environ["SLACK_APP_TOKEN"])
|
|
213
|
+
await self.socket_handler.start_async()
|
|
214
|
+
logger.info("Slack socket connection established")
|
|
215
|
+
|
|
216
|
+
def _start_health_monitoring(self):
|
|
217
|
+
"""Start health monitoring thread if configured."""
|
|
218
|
+
if not self.health_check_url:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
def health_ping_loop():
|
|
222
|
+
"""Health ping loop in background thread."""
|
|
223
|
+
ping_interval = int(os.getenv("HEALTH_PING_INTERVAL_SECONDS", "120"))
|
|
224
|
+
logger.info(f"Starting health ping to {self.health_check_url} every {ping_interval}s")
|
|
225
|
+
|
|
226
|
+
while True:
|
|
227
|
+
try:
|
|
228
|
+
response = requests.get(self.health_check_url, timeout=5)
|
|
229
|
+
if response.status_code == 200:
|
|
230
|
+
logger.info(f"Health ping successful: {response.text}")
|
|
231
|
+
else:
|
|
232
|
+
logger.warning(f"Health ping returned status: {response.status_code}")
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.error(f"Health ping failed: {str(e)}")
|
|
235
|
+
|
|
236
|
+
time.sleep(ping_interval)
|
|
237
|
+
|
|
238
|
+
self.health_thread = threading.Thread(target=health_ping_loop, daemon=True)
|
|
239
|
+
self.health_thread.start()
|
|
240
|
+
logger.info("Health monitoring started")
|
|
241
|
+
|
|
242
|
+
# Event handlers
|
|
243
|
+
async def _handle_app_mention(self, event, say):
|
|
244
|
+
"""Handle app mention events."""
|
|
245
|
+
logger.info(f"App mention event: ts={event.get('ts')}")
|
|
246
|
+
|
|
247
|
+
async def _handle_user_message(self, event, say):
|
|
248
|
+
"""Handle user messages with intelligent routing."""
|
|
249
|
+
ts = event.get("ts")
|
|
250
|
+
thread_ts = event.get("thread_ts")
|
|
251
|
+
channel = event.get("channel")
|
|
252
|
+
channel_type = event.get("channel_type")
|
|
253
|
+
|
|
254
|
+
logger.info(f"Message: ts={ts}, thread_ts={thread_ts}, channel={channel}, type={channel_type}")
|
|
255
|
+
|
|
256
|
+
# Check if message should be processed
|
|
257
|
+
if not await self._should_process_message(event):
|
|
258
|
+
logger.info("Skipping message processing")
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
text = event.get("text", "")
|
|
262
|
+
|
|
263
|
+
# Process the message
|
|
264
|
+
response_type, content = await self._process_message(text, event)
|
|
265
|
+
|
|
266
|
+
# Handle different response types
|
|
267
|
+
if response_type == "none":
|
|
268
|
+
logger.info("No response needed")
|
|
269
|
+
return
|
|
270
|
+
elif response_type == "emoji":
|
|
271
|
+
await self._send_emoji_reaction(content)
|
|
272
|
+
elif response_type == "message":
|
|
273
|
+
await self._send_text_response(content, event, say)
|
|
274
|
+
else:
|
|
275
|
+
logger.warning(f"Unknown response type: {response_type}")
|
|
276
|
+
|
|
277
|
+
async def _handle_reaction_added(self, event, say):
|
|
278
|
+
"""Handle reaction added events."""
|
|
279
|
+
try:
|
|
280
|
+
user = event.get("user")
|
|
281
|
+
emoji = event.get("reaction")
|
|
282
|
+
item_ts = event.get("item", {}).get("ts")
|
|
283
|
+
|
|
284
|
+
logger.info(f"Reaction added: {emoji} by {user} on {item_ts}")
|
|
285
|
+
|
|
286
|
+
# Find message and thread
|
|
287
|
+
message, thread = await self._find_message_and_thread(item_ts)
|
|
288
|
+
if message and thread:
|
|
289
|
+
if thread.add_reaction(message.id, emoji, user):
|
|
290
|
+
await self.thread_store.save(thread)
|
|
291
|
+
logger.info(f"Stored reaction {emoji}")
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.error(f"Error handling reaction: {str(e)}")
|
|
294
|
+
|
|
295
|
+
async def _handle_reaction_removed(self, event, say):
|
|
296
|
+
"""Handle reaction removed events."""
|
|
297
|
+
try:
|
|
298
|
+
user = event.get("user")
|
|
299
|
+
emoji = event.get("reaction")
|
|
300
|
+
item_ts = event.get("item", {}).get("ts")
|
|
301
|
+
|
|
302
|
+
logger.info(f"Reaction removed: {emoji} by {user} on {item_ts}")
|
|
303
|
+
|
|
304
|
+
# Find message and thread
|
|
305
|
+
message, thread = await self._find_message_and_thread(item_ts)
|
|
306
|
+
if message and thread:
|
|
307
|
+
if thread.remove_reaction(message.id, emoji, user):
|
|
308
|
+
await self.thread_store.save(thread)
|
|
309
|
+
logger.info(f"Removed reaction {emoji}")
|
|
310
|
+
except Exception as e:
|
|
311
|
+
logger.error(f"Error handling reaction removal: {str(e)}")
|
|
312
|
+
|
|
313
|
+
# Helper methods
|
|
314
|
+
async def _should_process_message(self, event):
|
|
315
|
+
"""Determine if a message should be processed."""
|
|
316
|
+
ts = str(event.get("ts"))
|
|
317
|
+
channel_type = event.get("channel_type")
|
|
318
|
+
thread_ts = event.get("thread_ts")
|
|
319
|
+
|
|
320
|
+
# Check if already processed
|
|
321
|
+
try:
|
|
322
|
+
messages = await self.thread_store.find_messages_by_attribute("platforms.slack.ts", ts)
|
|
323
|
+
if messages:
|
|
324
|
+
logger.info(f"Message {ts} already processed")
|
|
325
|
+
return False
|
|
326
|
+
except Exception as e:
|
|
327
|
+
logger.warning(f"Error checking message: {str(e)}")
|
|
328
|
+
|
|
329
|
+
# Process DMs, threaded messages, and channel messages
|
|
330
|
+
if channel_type == "im" or thread_ts:
|
|
331
|
+
return True
|
|
332
|
+
|
|
333
|
+
return True # For now, process all channel messages
|
|
334
|
+
|
|
335
|
+
async def _process_message(self, text: str, event: dict):
|
|
336
|
+
"""Process a message and return (type, content) tuple."""
|
|
337
|
+
try:
|
|
338
|
+
# Get or create thread
|
|
339
|
+
thread = await self._get_or_create_thread(event)
|
|
340
|
+
|
|
341
|
+
# Create user message
|
|
342
|
+
user_id = event.get("user", "unknown_user")
|
|
343
|
+
user_message = Message(
|
|
344
|
+
role="user",
|
|
345
|
+
content=text,
|
|
346
|
+
source={"id": user_id, "type": "user"},
|
|
347
|
+
platforms={
|
|
348
|
+
"slack": {
|
|
349
|
+
"channel": event.get("channel"),
|
|
350
|
+
"ts": event.get("ts"),
|
|
351
|
+
"thread_ts": event.get("thread_ts") or event.get("ts")
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Add message to thread and save
|
|
357
|
+
thread.add_message(user_message)
|
|
358
|
+
await self.thread_store.save(thread)
|
|
359
|
+
|
|
360
|
+
# Run message classifier
|
|
361
|
+
classifier_thread = copy.deepcopy(thread)
|
|
362
|
+
thread_ts = event.get("thread_ts") or event.get("ts")
|
|
363
|
+
|
|
364
|
+
with weave.attributes({'env': os.getenv("ENV", "development"), 'event_id': thread_ts}):
|
|
365
|
+
_, classify_messages = await self.message_classifier_agent.go(classifier_thread)
|
|
366
|
+
|
|
367
|
+
# Parse classification result
|
|
368
|
+
classify_result = classify_messages[-1].content if classify_messages else None
|
|
369
|
+
if classify_result:
|
|
370
|
+
try:
|
|
371
|
+
classification = json.loads(classify_result)
|
|
372
|
+
response_type = classification.get("response_type", "full_response")
|
|
373
|
+
|
|
374
|
+
if response_type == "ignore":
|
|
375
|
+
logger.info(f"Classification: IGNORE - {classification.get('reasoning', '')}")
|
|
376
|
+
return ("none", "")
|
|
377
|
+
elif response_type == "emoji_reaction":
|
|
378
|
+
emoji = classification.get("suggested_emoji", "thumbsup")
|
|
379
|
+
logger.info(f"Classification: EMOJI ({emoji}) - {classification.get('reasoning', '')}")
|
|
380
|
+
return ("emoji", {
|
|
381
|
+
"ts": event.get("ts"),
|
|
382
|
+
"channel": event.get("channel"),
|
|
383
|
+
"emoji": emoji
|
|
384
|
+
})
|
|
385
|
+
except json.JSONDecodeError:
|
|
386
|
+
logger.warning(f"Failed to parse classification: {classify_result}")
|
|
387
|
+
|
|
388
|
+
# Add thinking emoji while processing
|
|
389
|
+
try:
|
|
390
|
+
await self.slack_app.client.reactions_add(
|
|
391
|
+
channel=event.get("channel"),
|
|
392
|
+
timestamp=event.get("ts"),
|
|
393
|
+
name="thinking_face"
|
|
394
|
+
)
|
|
395
|
+
except Exception as e:
|
|
396
|
+
logger.warning(f"Failed to add thinking emoji: {str(e)}")
|
|
397
|
+
|
|
398
|
+
# Process with main agent
|
|
399
|
+
with weave.attributes({'env': os.getenv("ENV", "development"), 'event_id': thread_ts}):
|
|
400
|
+
_, new_messages = await self.agent.go(thread.id)
|
|
401
|
+
|
|
402
|
+
# Get assistant response
|
|
403
|
+
assistant_messages = [m for m in new_messages if m.role == "assistant"]
|
|
404
|
+
assistant_message = assistant_messages[-1] if assistant_messages else None
|
|
405
|
+
|
|
406
|
+
if not assistant_message:
|
|
407
|
+
return ("message", "I apologize, but I couldn't generate a response.")
|
|
408
|
+
|
|
409
|
+
response_content = assistant_message.content
|
|
410
|
+
|
|
411
|
+
# Add dev footer if metrics available
|
|
412
|
+
if hasattr(assistant_message, 'metrics') and assistant_message.metrics:
|
|
413
|
+
footer = self._get_dev_footer(assistant_message.metrics)
|
|
414
|
+
if footer:
|
|
415
|
+
response_content += footer
|
|
416
|
+
|
|
417
|
+
return ("message", response_content)
|
|
418
|
+
|
|
419
|
+
except Exception as e:
|
|
420
|
+
logger.error(f"Error processing message: {e}", exc_info=True)
|
|
421
|
+
return ("message", f"I apologize, but I encountered an error: {str(e)}")
|
|
422
|
+
|
|
423
|
+
async def _get_or_create_thread(self, event):
|
|
424
|
+
"""Get or create a thread based on Slack event data."""
|
|
425
|
+
slack_platform_data = {
|
|
426
|
+
"channel": event.get("channel"),
|
|
427
|
+
"thread_ts": event.get("thread_ts") or event.get("ts"),
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
# Try to find existing thread
|
|
431
|
+
if event.get("thread_ts"):
|
|
432
|
+
try:
|
|
433
|
+
threads = await self.thread_store.find_by_platform("slack", {"thread_ts": str(event.get("thread_ts"))})
|
|
434
|
+
if threads:
|
|
435
|
+
return threads[0]
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.warning(f"Error finding thread: {str(e)}")
|
|
438
|
+
|
|
439
|
+
# Try by ts
|
|
440
|
+
ts = event.get("ts")
|
|
441
|
+
if ts:
|
|
442
|
+
try:
|
|
443
|
+
threads = await self.thread_store.find_by_platform("slack", {"thread_ts": str(ts)})
|
|
444
|
+
if threads:
|
|
445
|
+
return threads[0]
|
|
446
|
+
except Exception as e:
|
|
447
|
+
logger.warning(f"Error finding thread by ts: {str(e)}")
|
|
448
|
+
|
|
449
|
+
# Create new thread
|
|
450
|
+
thread = Thread(platforms={"slack": slack_platform_data})
|
|
451
|
+
await self.thread_store.save(thread)
|
|
452
|
+
logger.info(f"Created new thread {thread.id}")
|
|
453
|
+
return thread
|
|
454
|
+
|
|
455
|
+
async def _find_message_and_thread(self, item_ts):
|
|
456
|
+
"""Find a message and its thread by timestamp."""
|
|
457
|
+
try:
|
|
458
|
+
messages = await self.thread_store.find_messages_by_attribute("platforms.slack.ts", item_ts)
|
|
459
|
+
if not messages:
|
|
460
|
+
return None, None
|
|
461
|
+
|
|
462
|
+
message = messages[0]
|
|
463
|
+
thread = await self.thread_store.get_thread_by_message_id(message.id)
|
|
464
|
+
return message, thread
|
|
465
|
+
except Exception as e:
|
|
466
|
+
logger.error(f"Error finding message: {str(e)}")
|
|
467
|
+
return None, None
|
|
468
|
+
|
|
469
|
+
async def _send_emoji_reaction(self, reaction_info):
|
|
470
|
+
"""Send an emoji reaction."""
|
|
471
|
+
try:
|
|
472
|
+
await self.slack_app.client.reactions_add(
|
|
473
|
+
channel=reaction_info["channel"],
|
|
474
|
+
timestamp=reaction_info["ts"],
|
|
475
|
+
name=reaction_info["emoji"]
|
|
476
|
+
)
|
|
477
|
+
except Exception as e:
|
|
478
|
+
logger.error(f"Error sending emoji reaction: {str(e)}")
|
|
479
|
+
|
|
480
|
+
async def _send_text_response(self, text, event, say):
|
|
481
|
+
"""Send a text response."""
|
|
482
|
+
try:
|
|
483
|
+
# Convert to Slack blocks
|
|
484
|
+
thread_ts = event.get("thread_ts") or event.get("ts")
|
|
485
|
+
response_blocks = await self._convert_to_slack_blocks(text, thread_ts)
|
|
486
|
+
|
|
487
|
+
# Send response
|
|
488
|
+
response = await say(
|
|
489
|
+
thread_ts=thread_ts,
|
|
490
|
+
text=response_blocks["text"],
|
|
491
|
+
blocks=response_blocks["blocks"]
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Update assistant message with Slack timestamp
|
|
495
|
+
if response and "ts" in response:
|
|
496
|
+
thread = await self._get_or_create_thread(event)
|
|
497
|
+
await self._update_assistant_message_with_slack_ts(
|
|
498
|
+
thread,
|
|
499
|
+
event.get("channel"),
|
|
500
|
+
response["ts"],
|
|
501
|
+
thread_ts
|
|
502
|
+
)
|
|
503
|
+
except Exception as e:
|
|
504
|
+
logger.error(f"Error sending text response: {str(e)}")
|
|
505
|
+
|
|
506
|
+
async def _convert_to_slack_blocks(self, text, thread_ts=None):
|
|
507
|
+
"""Convert markdown text to Slack blocks."""
|
|
508
|
+
try:
|
|
509
|
+
with weave.attributes({'env': os.getenv("ENV", "development"), 'event_id': thread_ts}):
|
|
510
|
+
result = await generate_slack_blocks(content=text)
|
|
511
|
+
|
|
512
|
+
if result and isinstance(result, dict) and "blocks" in result:
|
|
513
|
+
return {"blocks": result["blocks"], "text": result.get("text", text)}
|
|
514
|
+
else:
|
|
515
|
+
return {"text": text}
|
|
516
|
+
except Exception as e:
|
|
517
|
+
logger.error(f"Error converting to Slack blocks: {e}")
|
|
518
|
+
return {"text": text}
|
|
519
|
+
|
|
520
|
+
async def _update_assistant_message_with_slack_ts(self, thread, channel, response_ts, thread_ts):
|
|
521
|
+
"""Update assistant message with Slack timestamp."""
|
|
522
|
+
try:
|
|
523
|
+
for message in reversed(thread.messages):
|
|
524
|
+
if message.role == "assistant" and (not message.platforms or "slack" not in message.platforms):
|
|
525
|
+
message.platforms = message.platforms or {}
|
|
526
|
+
message.platforms["slack"] = {
|
|
527
|
+
"channel": channel,
|
|
528
|
+
"ts": response_ts,
|
|
529
|
+
"thread_ts": thread_ts
|
|
530
|
+
}
|
|
531
|
+
await self.thread_store.save(thread)
|
|
532
|
+
logger.info(f"Updated assistant message with ts={response_ts}")
|
|
533
|
+
return True
|
|
534
|
+
return False
|
|
535
|
+
except Exception as e:
|
|
536
|
+
logger.error(f"Error updating assistant message: {str(e)}")
|
|
537
|
+
return False
|
|
538
|
+
|
|
539
|
+
def _get_dev_footer(self, metrics):
|
|
540
|
+
"""Generate dev footer from metrics."""
|
|
541
|
+
footer = f"\n\n{self.agent.name}: v{getattr(self.agent, 'version', '1.0.0')}"
|
|
542
|
+
|
|
543
|
+
model = metrics.get('model', 'N/A')
|
|
544
|
+
weave_url = None
|
|
545
|
+
if 'weave_call' in metrics:
|
|
546
|
+
weave_url = metrics['weave_call'].get('ui_url')
|
|
547
|
+
|
|
548
|
+
if model:
|
|
549
|
+
footer += f" {model}"
|
|
550
|
+
if weave_url:
|
|
551
|
+
footer += f" | [Weave trace]({weave_url})"
|
|
552
|
+
|
|
553
|
+
return footer if (weave_url or model) else ""
|
|
554
|
+
|
|
555
|
+
async def start(self, host: str = "0.0.0.0", port: int = 8000):
|
|
556
|
+
"""Start the Slack bot server."""
|
|
557
|
+
config = uvicorn.Config(
|
|
558
|
+
app=self.fastapi_app,
|
|
559
|
+
host=host,
|
|
560
|
+
port=port,
|
|
561
|
+
log_level="info",
|
|
562
|
+
workers=1,
|
|
563
|
+
loop="asyncio",
|
|
564
|
+
timeout_keep_alive=65,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
server = uvicorn.Server(config)
|
|
568
|
+
|
|
569
|
+
try:
|
|
570
|
+
logger.info(f"Starting server on {host}:{port}")
|
|
571
|
+
await server.serve()
|
|
572
|
+
except Exception as e:
|
|
573
|
+
logger.error(f"Server error: {str(e)}", exc_info=True)
|
|
574
|
+
raise
|
|
575
|
+
finally:
|
|
576
|
+
logger.info("Server shutting down")
|