Skip to content

Commit 26c0a34

Browse files
authored
Merge branch 'main' into feat(webapp)-sso-ui-improvements
2 parents 983a442 + 71e4b00 commit 26c0a34

2 files changed

Lines changed: 142 additions & 0 deletions

File tree

docs/docs.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@
526526
"guides/example-projects/claude-changelog-generator",
527527
"guides/example-projects/claude-github-wiki",
528528
"guides/example-projects/claude-thinking-chatbot",
529+
"guides/example-projects/clickhouse-chat-agent",
529530
"guides/example-projects/cursor-background-agent",
530531
"guides/example-projects/human-in-the-loop-workflow",
531532
"guides/example-projects/mastra-agents-with-memory",
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
---
2+
title: "ClickHouse chat agent"
3+
sidebarTitle: "ClickHouse chat agent"
4+
description: "Build a chat agent that answers questions about your data by writing and running SQL against ClickHouse Cloud, using chat.agent() and the ClickHouse Node.js client."
5+
---
6+
7+
## Overview
8+
9+
This example is a [chat agent](/ai-chat/overview) that answers natural-language questions about the data in a [ClickHouse Cloud](https://clickhouse.com/cloud) database. The agent discovers the schema, writes ClickHouse SQL, runs it through the official [ClickHouse Node.js client](https://clickhouse.com/docs/integrations/javascript), and streams back answers with markdown tables. Trigger.dev handles the chat session, turn loop, streaming, and resumability — the whole agent is one `chat.agent()` call and three tools.
10+
11+
**Tech stack:**
12+
13+
- **[Trigger.dev AI chat](/ai-chat/overview)** for the agent session, turn loop, and streaming
14+
- **[ClickHouse Node.js client](https://clickhouse.com/docs/integrations/javascript)** (`@clickhouse/client`) for queries over HTTPS
15+
- **[AI SDK](https://ai-sdk.dev/)** with Anthropic Claude for the model and tool calling
16+
17+
**Features:**
18+
19+
- **Schema discovery tools**: `listTables` reads table names, engines, and row counts from `system.tables`; `describeTable` returns column names and types using a bound `Identifier` query param, so table names are never interpolated into SQL strings
20+
- **Read-only query tool**: `runQuery` accepts SELECT-style statements only, enforced in code and backed by ClickHouse settings — `readonly=2`, a 1,000-row result cap, and a 30 second execution timeout
21+
- **Self-correcting SQL**: query errors are returned to the model as tool output, so the agent reads the ClickHouse error, fixes its SQL, and retries
22+
- **Single environment variable**: the ClickHouse connection is one `CLICKHOUSE_URL` with the credentials embedded, set in the Trigger.dev dashboard
23+
24+
## GitHub repo
25+
26+
<Card
27+
title="View the ClickHouse chat agent repo"
28+
icon="GitHub"
29+
href="https://github.com/triggerdotdev/examples/tree/main/clickhouse-chat-agent"
30+
>
31+
Click here to view the full code for this project in our examples repository on GitHub. You can
32+
fork it and use it as a starting point for your own project.
33+
</Card>
34+
35+
## How it works
36+
37+
### The agent
38+
39+
The agent is defined with [`chat.agent()`](/ai-chat/overview). Tools are declared on the config so tool results survive history re-conversion across turns, and the `run` function returns a `streamText()` call:
40+
41+
```ts trigger/clickhouse-agent.ts
42+
import { chat } from "@trigger.dev/sdk/ai";
43+
import { anthropic } from "@ai-sdk/anthropic";
44+
import { stepCountIs, streamText } from "ai";
45+
46+
export const clickhouseAgent = chat.agent({
47+
id: "clickhouse-agent",
48+
idleTimeoutInSeconds: 300,
49+
tools: { listTables, describeTable, runQuery },
50+
run: async ({ messages, tools, signal }) => {
51+
return streamText({
52+
// Spread chat.toStreamTextOptions() FIRST — it wires up
53+
// prepareStep (compaction, steering, background injection),
54+
// the system prompt set via chat.prompt(), and telemetry.
55+
...chat.toStreamTextOptions(),
56+
model: anthropic("claude-opus-4-8"),
57+
system: SYSTEM_PROMPT,
58+
messages,
59+
tools,
60+
stopWhen: stepCountIs(15),
61+
abortSignal: signal,
62+
});
63+
},
64+
});
65+
```
66+
67+
The system prompt tells the agent to explore the schema before querying, write ClickHouse SQL (not Postgres dialect), prefer aggregations, and present results as markdown tables.
68+
69+
### The query tool
70+
71+
`runQuery` guards against writes twice: a statement allowlist in code, and ClickHouse settings on the request itself. Errors are returned to the model instead of thrown, which is what makes the agent self-correct:
72+
73+
```ts trigger/clickhouse-agent.ts
74+
const READ_ONLY_STATEMENTS = /^\s*(select|with|show|describe|desc|explain|exists)\b/i;
75+
76+
const runQuery = tool({
77+
description:
78+
"Run a read-only SQL query against ClickHouse and get the results as JSON rows.",
79+
inputSchema: z.object({
80+
query: z.string().describe("The ClickHouse SQL query to run"),
81+
}),
82+
execute: async ({ query }) => {
83+
if (!READ_ONLY_STATEMENTS.test(query)) {
84+
return { error: "Only read-only statements are allowed." };
85+
}
86+
try {
87+
const result = await getClickHouse().query({
88+
query,
89+
format: "JSONEachRow",
90+
clickhouse_settings: {
91+
// readonly=2: reads only (no writes/DDL), but per-query settings
92+
// like the limits below are still allowed.
93+
readonly: "2",
94+
max_result_rows: "1000",
95+
result_overflow_mode: "break",
96+
max_execution_time: 30,
97+
},
98+
});
99+
const rows = await result.json();
100+
return { rowCount: rows.length, rows };
101+
} catch (error) {
102+
// Return ClickHouse errors to the model so it can fix the query and retry.
103+
return { error: error instanceof Error ? error.message : String(error) };
104+
}
105+
},
106+
});
107+
```
108+
109+
### Connecting to ClickHouse
110+
111+
The client reads a single `CLICKHOUSE_URL` environment variable — the HTTPS endpoint with credentials embedded — set in the Trigger.dev dashboard on the [Environment Variables page](/deploy-environment-variables):
112+
113+
```bash
114+
CLICKHOUSE_URL=https://default:YOUR_PASSWORD@YOUR_SERVICE.clickhouse.cloud:8443
115+
```
116+
117+
```ts trigger/clickhouse-agent.ts
118+
import { createClient } from "@clickhouse/client";
119+
120+
const clickhouse = createClient({ url: process.env.CLICKHOUSE_URL });
121+
```
122+
123+
### Chatting with the agent
124+
125+
Run `npx trigger.dev@latest dev`, then open the **AI agents** page in the dashboard and chat with `clickhouse-agent` in the playground. With a dataset like [NYC Taxi](https://clickhouse.com/docs/getting-started/example-datasets/nyc-taxi) loaded, asking "What were the top 5 busiest pickup days?" produces a `listTables` call, a `describeTable` call, a SQL aggregation, and a streamed markdown table of results.
126+
127+
## Relevant code
128+
129+
- **Agent + tools**: [trigger/clickhouse-agent.ts](https://github.com/triggerdotdev/examples/blob/main/clickhouse-chat-agent/trigger/clickhouse-agent.ts): the `chat.agent()` definition, the three tools, the read-only guards, and the ClickHouse client
130+
- **Trigger config**: [trigger.config.ts](https://github.com/triggerdotdev/examples/blob/main/clickhouse-chat-agent/trigger.config.ts): project config pointing at the `trigger/` directory
131+
132+
## Learn more
133+
134+
<CardGroup cols={2}>
135+
<Card title="AI chat overview" icon="message-bot" href="/ai-chat/overview">
136+
How chat agents, sessions, and the turn loop work.
137+
</Card>
138+
<Card title="Tools" icon="wrench" href="/ai-chat/tools">
139+
Declaring tools on your agent and how they persist across turns.
140+
</Card>
141+
</CardGroup>

0 commit comments

Comments
 (0)