Skip to content

Commit 7b32701

Browse files
authored
Merge pull request #311 from sdairs/slackbot_viz
add slackbot viz capability
2 parents 206dbd0 + b1116b9 commit 7b32701

File tree

5 files changed

+275
-16
lines changed

5 files changed

+275
-16
lines changed

ai/mcp/slackbot/.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
.venv
2-
.env
2+
.env
3+
__pycache__
4+
*.pyc

ai/mcp/slackbot/README.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
# Slack ClickHouse AI Bot
22

3-
This bot allows you to ask questions about your ClickHouse data directly from Slack, using natural language. It uses the ClickHouse MCP server and PydanticAI.
3+
This bot allows you to ask questions about your ClickHouse data directly from Slack, using natural language. It uses the ClickHouse MCP server and PydanticAI with integrated data visualization capabilities.
44

55
---
66

77
## Features
88
- **Ask in Slack:** Mention the bot in a channel, reply to a thread, or DM the bot with your question.
99
- **AI-Powered:** Uses Anthropic Claude (via Pydantic AI SDK) to interpret questions and generate SQL.
1010
- **ClickHouse Integration:** Queries your ClickHouse database using MCP.
11+
- **Data Visualization:** Automatically generates charts and graphs from query results using Vega-Lite specifications.
12+
- **Local Chart Rendering:** Creates beautiful PNG charts locally and uploads them directly to Slack threads.
1113
- **Thread Awareness:** When replying in a thread, the bot uses the full conversation history as context.
1214

1315
---
@@ -42,6 +44,7 @@ This bot allows you to ask questions about your ClickHouse data directly from Sl
4244
- `im:read`
4345
- `im:write`
4446
- `channels:history`
47+
- `files:write`
4548
- Install the app to your workspace and note down the **Bot User OAuth Token** for the environment variable `SLACK_BOT_TOKEN`.
4649
- Go to **Event Subscriptions**
4750
- Enable **Events**
@@ -77,24 +80,35 @@ You can adapt the ClickHouse variables to use your own ClickHouse server. If you
7780
uv run main.py
7881
```
7982
2. **In Slack:**
80-
- Mention the bot in a channel: `@yourbot Who are the top contributors to the ClickHouse git repo?`
81-
- Reply to the thread with a mention: `@yourbot how many contributions did these users make last week?`
82-
- DM the bot: `Show me all tables in the demo database.`
83+
- **Query data:** `@yourbot Who are the top contributors to the ClickHouse git repo?`
84+
- **Request visualizations:** `@yourbot Show me a chart of the top 10 contributors`
85+
- **Follow-up in threads:** `@yourbot How many commits did these users make last month?`
86+
- **DM the bot:** `Show me all tables in the demo database and create a chart of table sizes`
8387

84-
The bot will reply in the thread, using all previous thread messages as context if applicable.
88+
The bot will reply in the thread with both text responses and chart visualizations when appropriate, using all previous thread messages as context.
8589

8690
---
8791

8892
**Thread Context:**
8993
When replying in a thread, the bot loads all previous messages (except the current one) and includes them as context for the AI.
9094

9195
**Tool Usage:**
92-
The bot uses only the tools available via MCP (e.g., schema discovery, SQL execution) and will always show the SQL used and a summary of how the answer was found.
96+
The bot uses ClickHouse MCP tools for database operations (schema discovery, SQL execution) and automatically generates visualizations using Vega-Lite when appropriate. Charts are rendered locally and uploaded as PNG images to Slack.
97+
98+
**Chart Types Supported:**
99+
- Bar charts for categorical comparisons
100+
- Line charts for time series data
101+
- Scatter plots for correlations
102+
- Pie charts for proportions
103+
- Area charts for trends over time
104+
- All standard Vega-Lite chart types
93105

94106
---
95107

96108
## Tools used
97-
- [Pydantic AI SDK](https://github.com/pydantic/pydantic-ai)
98-
- [Slack Bolt](https://slack.dev/bolt-python/)
99-
- [ClickHouse](https://clickhouse.com/)
100-
- [ClickHouse MCP](https://github.com/ClickHouse/mcp-clickhouse)
109+
- [Pydantic AI SDK](https://github.com/pydantic/pydantic-ai) - AI agent framework
110+
- [Slack Bolt](https://slack.dev/bolt-python/) - Slack app integration
111+
- [ClickHouse](https://clickhouse.com/) - Database platform
112+
- [ClickHouse MCP](https://github.com/ClickHouse/mcp-clickhouse) - Database connectivity
113+
- [Altair](https://altair-viz.github.io/) - Vega-Lite Python API
114+
- [vl-convert](https://github.com/vega/vl-convert) - Chart rendering engine

ai/mcp/slackbot/main.py

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import os
22
import logging
33
import asyncio
4+
import tempfile
5+
import json
6+
import re
47

58
from dotenv import load_dotenv
69
from slack_bolt.async_app import AsyncApp
710
from slack_bolt.adapter.socket_mode.aiohttp import AsyncSocketModeHandler
811
from slack_sdk.web.async_client import AsyncWebClient
912
from pydantic_ai import Agent
1013
from pydantic_ai.mcp import MCPServerStdio
14+
import altair as alt
15+
import vl_convert as vlc
1116

1217
# Load environment variables from .env file
1318
load_dotenv()
@@ -30,25 +35,103 @@
3035
logging.basicConfig(level=logging.INFO)
3136

3237
# --- MCP SERVER AND AGENT SETUP ---
33-
mcp_server = MCPServerStdio(
38+
clickhouse_server = MCPServerStdio(
3439
'uv',
3540
args=[
3641
'run',
3742
'--with', 'mcp-clickhouse',
3843
'--python', '3.13',
3944
'mcp-clickhouse'
4045
],
41-
env=CLICKHOUSE_ENV
46+
env=CLICKHOUSE_ENV,
47+
timeout=30.0
4248
)
4349

4450
agent = Agent(
4551
"anthropic:claude-sonnet-4-0",
46-
mcp_servers=[mcp_server],
47-
system_prompt="You are a data assistant. You have access to a ClickHouse database from which you can answer the user's questions. You have tools available to you that let you explore the database, e.g. to list available databases, tables, etc., and to execute SQL queries against them. Use these tools to answer the user's questions. You must always answer the user's questions by using the available tools. If the database cannot help you, say so. You must include a summary of how you came to your answer: e.g. which data you used and how you queried it."
52+
mcp_servers=[clickhouse_server],
53+
system_prompt="""You are a data assistant with visualization capabilities. You have access to a ClickHouse database and can create charts from query results.
54+
55+
Available capabilities:
56+
1) ClickHouse tools to explore databases, tables, and execute SQL queries
57+
2) Chart generation by providing Vega-Lite specifications
58+
59+
When users ask for data analysis with visualizations:
60+
1. First query the database using available tools
61+
2. If a visualization would be helpful, create a Vega-Lite chart specification
62+
3. Format your Vega-Lite spec as JSON within ```json blocks
63+
4. Choose appropriate chart types: bar charts for categories, line charts for time series, scatter for correlations, pie for proportions
64+
65+
Example Vega-Lite specification format:
66+
```json
67+
{
68+
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
69+
"title": "Chart Title",
70+
"data": {"values": [{"category": "A", "value": 100}, {"category": "B", "value": 200}]},
71+
"mark": "bar",
72+
"encoding": {
73+
"x": {"field": "category", "type": "nominal"},
74+
"y": {"field": "value", "type": "quantitative"}
75+
}
76+
}
77+
```
78+
79+
Always include a summary of your approach: what data you used, how you queried it, and why you chose a specific visualization."""
4880
)
4981

5082
app = AsyncApp(token=SLACK_BOT_TOKEN)
5183

84+
async def render_and_upload_chart(client, channel, thread_ts, vega_lite_spec, title="Chart"):
85+
"""Render Vega-Lite spec to PNG and upload to Slack"""
86+
try:
87+
# Parse the Vega-Lite specification
88+
if isinstance(vega_lite_spec, str):
89+
spec = json.loads(vega_lite_spec)
90+
else:
91+
spec = vega_lite_spec
92+
93+
# Render to PNG using vl-convert
94+
png_data = vlc.vegalite_to_png(spec)
95+
96+
# Create temporary file
97+
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp_file:
98+
tmp_file.write(png_data)
99+
tmp_file.flush()
100+
101+
# Upload file to Slack
102+
response = await client.files_upload_v2(
103+
channel=channel,
104+
file=tmp_file.name,
105+
title=title,
106+
thread_ts=thread_ts
107+
)
108+
109+
# Clean up temp file
110+
os.unlink(tmp_file.name)
111+
return response
112+
113+
except Exception as e:
114+
logging.error(f"Error rendering and uploading chart: {e}")
115+
return None
116+
117+
def extract_vega_lite_specs(text):
118+
"""Extract Vega-Lite JSON specifications from text"""
119+
# Look for JSON blocks that contain Vega-Lite specs
120+
json_pattern = r'```json\s*(\{.*?\})\s*```'
121+
matches = re.findall(json_pattern, text, re.DOTALL)
122+
123+
specs = []
124+
for match in matches:
125+
try:
126+
spec = json.loads(match)
127+
# Check if it looks like a Vega-Lite spec
128+
if "$schema" in spec and "vega" in spec["$schema"]:
129+
specs.append(spec)
130+
except json.JSONDecodeError:
131+
continue
132+
133+
return specs
134+
52135
async def handle_slack_query(event, say):
53136
user = event["user"]
54137
text = event.get("text", "")
@@ -81,7 +164,25 @@ async def do_agent():
81164

82165
async with agent.run_mcp_servers():
83166
result = await agent.run(prompt)
84-
await say(text=f"{result.output}", thread_ts=thread_ts)
167+
168+
# Check if the response contains Vega-Lite chart specifications
169+
response_text = result.output
170+
client = AsyncWebClient(token=SLACK_BOT_TOKEN)
171+
172+
# Extract Vega-Lite specifications from the response
173+
vega_specs = extract_vega_lite_specs(response_text)
174+
175+
if vega_specs:
176+
# Render and upload each chart found
177+
for i, spec in enumerate(vega_specs):
178+
chart_title = spec.get("title", f"Chart {i+1}" if len(vega_specs) > 1 else "Chart")
179+
await render_and_upload_chart(client, channel, thread_ts, spec, chart_title)
180+
181+
# Remove JSON blocks from text response to avoid clutter
182+
clean_text = re.sub(r'```json\s*\{.*?\}\s*```', '[Chart uploaded above]', response_text, flags=re.DOTALL)
183+
await say(text=clean_text, thread_ts=thread_ts)
184+
else:
185+
await say(text=response_text, thread_ts=thread_ts)
85186

86187
asyncio.create_task(do_agent())
87188

ai/mcp/slackbot/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ dependencies = [
1010
"pydantic-ai>=0.3.5",
1111
"python-dotenv>=1.1.1",
1212
"slack-bolt>=1.23.0",
13+
"altair>=5.0.0",
14+
"vl-convert-python>=1.6.0",
15+
"pillow>=10.0.0",
1316
]

0 commit comments

Comments
 (0)