Skip to content
This repository was archived by the owner on Apr 13, 2026. It is now read-only.

Commit fc33293

Browse files
committed
Update README.md
hyphenated directory Slackbot code clean up Handle Slack event errors
1 parent c628e90 commit fc33293

8 files changed

Lines changed: 157 additions & 110 deletions

File tree

dev-utils/data-generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import random
55
from datetime import datetime, timedelta
66

7-
OUTPUT_DIR = "./generated_days"
7+
OUTPUT_DIR = "./generated-days"
88
DAYS = 5
99
SESSIONS_PER_DAY = 3
1010
SESSION_LENGTH_MIN = 15 # minutes (increased for better data duration)

installer/data-downloader/docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,7 @@ services:
3838
- api
3939
networks:
4040
- datalink
41+
42+
networks:
43+
datalink:
44+
external: true

installer/docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ services:
8282
slackbot:
8383
build: ./slackbot
8484
container_name: slackbot
85-
restart: no
86-
# set restart to unless-stopped if you intend to run the slackbot continuously
85+
restart: unless-stopped
86+
# set restart to no if you want to manually control the slackbot
8787
environment:
8888
SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN:-}
8989
SLACK_APP_TOKEN: ${SLACK_APP_TOKEN:-}

installer/sandbox/README.md

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,40 @@ Generate and execute Python code for telemetry analysis using Cohere AI and a cu
44

55
## Architecture
66

7-
```
8-
[User in Slack] → [Slackbot (Lappy)] → [Code Generator] → [Custom Sandbox]
9-
↑ (Cohere AI) (Python + InfluxDB)
10-
└──────────────────────────── [Results (images, logs, data)] ←──────────┘
7+
```mermaid
8+
flowchart TD
9+
User([User in Slack])
10+
11+
User -->|"!agent <prompt>"| Slackbot
12+
13+
subgraph "Code Generator (port 3030)"
14+
direction TB
15+
Receive[Receive prompt]
16+
LoadGuide["Load system prompt (prompt-guide.txt)"]
17+
CohereAI["Cohere AI (command-r-plus)"]
18+
RetryNode["Append error to prompt and retry"]
19+
Check{"Execution successful?"}
20+
21+
Receive --> LoadGuide
22+
LoadGuide -->|"system prompt + user prompt"| CohereAI
23+
CohereAI -->|"Generated Python code"| Check
24+
RetryNode -->|"Updated prompt"| CohereAI
25+
end
26+
27+
subgraph "Sandbox (port 8080)"
28+
direction TB
29+
Execute["Execute Python in subprocess"]
30+
InfluxDB[("InfluxDB3")]
31+
32+
Execute <-->|"slicks: fetch_telemetry(), discover_sensors()"| InfluxDB
33+
end
34+
35+
Slackbot -->|"POST /api/generate-code { prompt }"| Receive
36+
Check -->|"Submit code"| Execute
37+
Execute -->|"ok, stdout, stderr, output_files (b64)"| Check
38+
Check -->|"Failed & retries remaining"| RetryNode
39+
Check -->|"Success or max retries reached"| Slackbot
40+
Slackbot -->|"Text output + uploaded images"| User
1141
```
1242

1343
### Components
@@ -38,7 +68,7 @@ Add to your `.env` file:
3868
# Required: Cohere API key
3969
COHERE_API_KEY=your-cohere-api-key-here
4070

41-
# Optional: Cohere model (default: command-r-plus)
71+
# Optional: Cohere model (default in docker-compose: command-r-plus; code default: command-a-reasoning-08-2025)
4272
COHERE_MODEL=command-r-plus
4373

4474
# Optional: Max retry attempts (default: 2)
@@ -119,27 +149,27 @@ Use the `!agent` command in Slack:
119149
**Simple Plot:**
120150
```
121151
User: !agent create a random scatter plot
122-
Bot: 🤖 Processing your request...
123-
Bot: Code executed successfully!
124-
Bot: 📊 Here's your visualization: [output.png]
152+
Bot: Processing your request...
153+
Bot: Code executed successfully!
154+
Bot: Here's your visualization: [output.png]
125155
```
126156

127157
**With Retry:**
128158
```
129159
User: !agent plot df['time'] data
130-
Bot: 🤖 Processing your request...
131-
Bot: ⚠️ Initial code had errors. Retried 1 time(s) with error feedback.
132-
Bot: Code executed successfully! (after 1 retry)
133-
Bot: Output: Data plotted successfully
160+
Bot: Processing your request...
161+
Bot: Initial code had errors. Retried 1 time(s) with error feedback.
162+
Bot: Code executed successfully! (after 1 retry)
163+
Bot: Output: Data plotted successfully
134164
```
135165

136166
**Failure After Retries:**
137167
```
138168
User: !agent impossible task
139-
Bot: 🤖 Processing your request...
140-
Bot: ⚠️ Initial code had errors. Retried 2 time(s) with error feedback.
141-
Bot: Code execution failed after 2 retries:
142-
ERROR_TRACE: [error details]
169+
Bot: Processing your request...
170+
Bot: Initial code had errors. Retried 2 time(s) with error feedback.
171+
Bot: Code execution failed after 2 retries:
172+
ERROR_TRACE: [error details]
143173
```
144174

145175
## System Prompt
@@ -204,9 +234,9 @@ Response:
204234
}
205235
```
206236

207-
### Sandbox Runner Service (Port 9090)
237+
### Sandbox Runner Service (Port 8080)
208238

209-
**POST /**
239+
**POST / or /execute**
210240
```json
211241
{
212242
"code": "print('hello world')"
@@ -216,11 +246,16 @@ Response:
216246
Response:
217247
```json
218248
{
219-
"success": true,
249+
"ok": true,
220250
"std_out": "hello world\n",
221251
"std_err": "",
222252
"return_code": 0,
223-
"output_files": []
253+
"output_files": [
254+
{
255+
"filename": "output.png",
256+
"b64_data": "base64-encoded-bytes"
257+
}
258+
]
224259
}
225260
```
226261

@@ -241,7 +276,7 @@ docker compose logs -f sandbox
241276

242277
- Code executes in isolated subprocess with timeout limits
243278
- **Has internet access** for InfluxDB queries via `slicks` and API calls
244-
- Limited runtime (30 seconds max, configurable)
279+
- Limited runtime (120 seconds max, configurable via `SANDBOX_TIMEOUT`)
245280
- Limited memory and file size
246281
- InfluxDB credentials passed via environment variables (consumed by `slicks` automatically)
247282
- Generated code is logged for audit purposes

installer/slackbot/README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ Set the following environment variables before running the bot:
2525
- `SLACK_WEBHOOK_URL` (optional): Incoming webhook URL to announce when the bot starts.
2626
- `SLACK_DEFAULT_CHANNEL` (optional): Channel ID the bot monitors and posts to. Defaults to `C08NTG6CXL5`.
2727
- `SLACK_BOT_USER_ID` (optional): Bot user ID. Used to avoid responding to itself. Default is `U08P8KS8K25`.
28-
- `AGENT_PAYLOAD_PATH` (optional): Path where `!agent` instructions are written. Defaults to `agent_payload.txt` in the working directory.
29-
- `AGENT_TRIGGER_COMMAND` (optional): Shell command to execute after writing the payload. When omitted, the bot runs `python3 -c "print('Agent trigger placeholder executed')"` as a stand-in.
28+
- `CODE_GENERATOR_URL` (optional): URL of the code-generator service. Defaults to `http://code-generator:3030`.
3029

3130
Local Development
3231
-----------------
@@ -72,14 +71,18 @@ Slack Commands
7271
Upload the bundled `lappy_test_image.png` to confirm file upload functionality.
7372

7473
- `!agent <instructions>`
75-
Write the provided text to `AGENT_PAYLOAD_PATH` and run `AGENT_TRIGGER_COMMAND`. Replies with the stdout (or failure information) from the triggered process. If no instructions are supplied, the bot prompts the user to include them.
74+
Generate and execute Python code using AI via the code-generator service. Supports data visualization and analysis. Timeout: 120 seconds.
75+
76+
- `!agent-debug <instructions>`
77+
Extended version of `!agent` with 1200 second (20 minute) timeout. Automatically retries up to 2 times if code fails. Use for complex analysis or large datasets.
7678

7779
Agent Workflow
7880
--------------
79-
1. User posts `!agent` followed by freeform text.
80-
2. Bot saves the text (plus newline) to the payload file, creating parent directories if necessary.
81-
3. Bot runs the trigger command and reports success or failure in Slack.
82-
4. Another service can watch the payload file or integrate with the trigger command to act on the instructions.
81+
1. User posts `!agent` or `!agent-debug` followed by freeform instructions.
82+
2. Bot sends instructions to the code-generator service via HTTP.
83+
3. Code-generator uses AI to create Python code based on the instructions.
84+
4. Generated code executes in a sandboxed environment and returns results (output, images, etc.).
85+
5. Bot reports success/failure and uploads any generated visualizations to Slack.
8386

8487
Helper Functions
8588
----------------

installer/slackbot/agent_payload.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

installer/slackbot/slack_bot.py

Lines changed: 84 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import os
2-
import shlex
3-
import subprocess
42
import base64
53
import json
64
import datetime
@@ -37,15 +35,7 @@
3735

3836
WEBHOOK_URL = os.environ.get("SLACK_WEBHOOK_URL")
3937
DEFAULT_CHANNEL = os.environ.get("SLACK_DEFAULT_CHANNEL", "C08NTG6CXL5")
40-
AGENT_PAYLOAD_PATH = Path(os.environ.get("AGENT_PAYLOAD_PATH", "agent_payload.txt"))
41-
AGENT_TRIGGER_COMMAND = os.environ.get("AGENT_TRIGGER_COMMAND")
4238
CODE_GENERATOR_URL = os.environ.get("CODE_GENERATOR_URL", "http://code-generator:3030")
43-
DEFAULT_AGENT_COMMAND = [
44-
"python3",
45-
"-c",
46-
"print('Agent trigger placeholder executed')",
47-
]
48-
AGENT_COMMAND = shlex.split(AGENT_TRIGGER_COMMAND) if AGENT_TRIGGER_COMMAND else DEFAULT_AGENT_COMMAND
4939

5040

5141
# --- Public helper functions ---
@@ -308,78 +298,94 @@ def handle_help(user, thread_ts=None, channel=None):
308298

309299
# --- Event Processing Logic ---
310300
def process_events(client: SocketModeClient, req: SocketModeRequest):
311-
if req.type != "events_api":
312-
return
313-
314-
client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id))
315-
event = req.payload.get("event", {})
316-
if event.get("type") != "message" or event.get("subtype") is not None:
317-
return
318-
319-
# Get channel type - check if it's a DM or the default channel
320-
channel = event.get("channel")
321-
channel_type = event.get("channel_type")
322-
323-
# Allow messages from default channel or DMs (im = direct message)
324-
is_dm = channel_type == "im"
325-
is_default_channel = channel == DEFAULT_CHANNEL
326-
327-
if not (is_dm or is_default_channel):
328-
return
329-
330-
msg_ts = event.get("ts")
331-
if msg_ts in processed_messages:
332-
print(f"Skipping already processed message: {msg_ts}")
333-
return
334-
335-
processed_messages.add(msg_ts)
336-
if len(processed_messages) > 1000:
337-
oldest_ts = sorted(processed_messages)[0]
338-
processed_messages.remove(oldest_ts)
339-
340-
user = event.get("user")
341-
bot_user_id = os.environ.get("SLACK_BOT_USER_ID", "U08P8KS8K25")
342-
if user == bot_user_id:
343-
print(f"Skipping message from bot itself ({bot_user_id}).")
344-
return
345-
346-
text = event.get("text", "").strip()
347-
if not text.startswith("!"):
348-
return
349-
350-
command_full = text[1:]
351-
command_parts = command_full.split()
352-
main_command = command_parts[0] if command_parts else ""
301+
try:
302+
if req.type != "events_api":
303+
return
353304

354-
print(
355-
f"Received command: '{command_full}' from user {user} "
356-
f"in {'DM' if is_dm else f'channel {channel}'}"
357-
)
305+
client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id))
306+
event = req.payload.get("event", {})
307+
if event.get("type") != "message" or event.get("subtype") is not None:
308+
return
358309

359-
# Get thread_ts - use the message timestamp to create/reply to thread
360-
# For DMs, thread_ts is the same as msg_ts
361-
thread_ts = event.get("thread_ts") or msg_ts
362-
363-
# For DM responses, use the DM channel; otherwise use DEFAULT_CHANNEL
364-
response_channel = channel if is_dm else DEFAULT_CHANNEL
365-
366-
if main_command == "location":
367-
handle_location(user, thread_ts, response_channel)
368-
elif main_command == "testimage":
369-
handle_testimage(user, thread_ts, response_channel)
370-
elif main_command == "agent":
371-
handle_agent(user, command_full, thread_ts, timeout=120, channel=response_channel)
372-
elif main_command == "agent-debug":
373-
handle_agent(user, command_full, thread_ts, timeout=1200, channel=response_channel)
374-
elif main_command == "help":
375-
handle_help(user, thread_ts, response_channel)
376-
else:
377-
send_slack_message(
378-
response_channel,
379-
text=f"❓ <@{user}> Unknown command: `{text}`. Try `!help`.",
380-
thread_ts=thread_ts,
310+
# Get channel type - check if it's a DM or the default channel
311+
channel = event.get("channel")
312+
channel_type = event.get("channel_type")
313+
314+
# Allow messages from default channel or DMs (im = direct message)
315+
is_dm = channel_type == "im"
316+
is_default_channel = channel == DEFAULT_CHANNEL
317+
318+
if not (is_dm or is_default_channel):
319+
return
320+
321+
msg_ts = event.get("ts")
322+
if msg_ts in processed_messages:
323+
print(f"Skipping already processed message: {msg_ts}")
324+
return
325+
326+
processed_messages.add(msg_ts)
327+
if len(processed_messages) > 1000:
328+
oldest_ts = sorted(processed_messages)[0]
329+
processed_messages.remove(oldest_ts)
330+
331+
user = event.get("user")
332+
bot_user_id = os.environ.get("SLACK_BOT_USER_ID", "U08P8KS8K25")
333+
if user == bot_user_id:
334+
print(f"Skipping message from bot itself ({bot_user_id}).")
335+
return
336+
337+
text = event.get("text", "").strip()
338+
if not text.startswith("!"):
339+
return
340+
341+
command_full = text[1:]
342+
command_parts = command_full.split()
343+
main_command = command_parts[0] if command_parts else ""
344+
345+
print(
346+
f"Received command: '{command_full}' from user {user} "
347+
f"in {'DM' if is_dm else f'channel {channel}'}"
381348
)
382349

350+
# Get thread_ts - use the message timestamp to create/reply to thread
351+
# For DMs, thread_ts is the same as msg_ts
352+
thread_ts = event.get("thread_ts") or msg_ts
353+
354+
# For DM responses, use the DM channel; otherwise use DEFAULT_CHANNEL
355+
response_channel = channel if is_dm else DEFAULT_CHANNEL
356+
357+
if main_command == "location":
358+
handle_location(user, thread_ts, response_channel)
359+
elif main_command == "testimage":
360+
handle_testimage(user, thread_ts, response_channel)
361+
elif main_command == "agent":
362+
handle_agent(user, command_full, thread_ts, timeout=120, channel=response_channel)
363+
elif main_command == "agent-debug":
364+
handle_agent(user, command_full, thread_ts, timeout=1200, channel=response_channel)
365+
elif main_command == "help":
366+
handle_help(user, thread_ts, response_channel)
367+
else:
368+
send_slack_message(
369+
response_channel,
370+
text=f"❓ <@{user}> Unknown command: `{text}`. Try `!help`.",
371+
thread_ts=thread_ts,
372+
)
373+
except Exception as e:
374+
import traceback
375+
print(f"❌ Error processing event: {e}")
376+
traceback.print_exc()
377+
try:
378+
# Try to notify the user about the error
379+
if 'user' in locals() and 'response_channel' in locals():
380+
send_slack_message(
381+
response_channel,
382+
text=f"❌ <@{user}> An error occurred while processing your command. Please try again later.",
383+
thread_ts=locals().get('thread_ts')
384+
)
385+
except:
386+
# If even error notification fails, just log it
387+
print("Failed to send error notification to Slack")
388+
383389

384390
# --- Main Execution ---
385391
if __name__ == "__main__":

0 commit comments

Comments
 (0)