Skip to content

Commit 434a5fb

Browse files
committed
multi-step tool workflow
1 parent f11f939 commit 434a5fb

File tree

5 files changed

+104
-11
lines changed

5 files changed

+104
-11
lines changed

apps/web/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"react-markdown": "^9.0.1",
2727
"recharts": "^2.15.0",
2828
"tailwind-merge": "^2.6.0",
29-
"tailwindcss-animate": "^1.0.7"
29+
"tailwindcss-animate": "^1.0.7",
30+
"zod": "^3.24.1"
3031
},
3132
"devDependencies": {
3233
"@eslint/eslintrc": "^3.2.0",

apps/web/pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/web/src/app/api/chat/route.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,69 @@
11
import { anthropic } from '@ai-sdk/anthropic';
2-
import { streamText } from 'ai';
2+
import { streamText, tool } from 'ai';
3+
import { getInstalledDataSources } from '@/lib/tinybird';
4+
import { z } from 'zod';
35

46
// Allow streaming responses up to 30 seconds
57
export const maxDuration = 30;
68

79
export async function POST(req: Request) {
10+
const token = req.headers.get('token') ?? '';
811
const { messages } = await req.json();
12+
console.log('token: ' + token)
913

1014
const result = streamText({
1115
model: anthropic('claude-3-5-sonnet-latest'),
16+
maxSteps: 5,
17+
tools: {
18+
getAvailableDataSources: tool({
19+
description: 'Get available data sources. ' +
20+
'This returns the names of data sources that the user currently has data available.' +
21+
'Only the data sources returned by the this tools should be used in future tools to query data.' +
22+
'This tool must always be used before analysing data.',
23+
parameters: z.object({
24+
// token: z.string().describe('Tinybird Admin Token used to authenticate calls to the Tinybird API'),
25+
}),
26+
execute: async () => {
27+
const dataSources = await getInstalledDataSources(token);
28+
return dataSources;
29+
},
30+
}),
31+
queryDataSource: tool({
32+
description: 'Query a data source. ' +
33+
'This tool should be used to query data sources that the user has data available.' +
34+
'Only the data sources returned by the getAvailableDataSources tool should be used as sources of data for this tool.' +
35+
'This tool should generate Tinybird SQL queries, which are based on a subset of ClickHouse SQL.' +
36+
'Every query MUST follow these rules:' +
37+
'1. The query must be a valid Tinybird SQL query' +
38+
'2. The query must be a SELECT query' +
39+
'3. The query must be a single query' +
40+
'4. The query must be a single table' +
41+
'5. The query must not end with a semicolon (;)' +
42+
'6. The query must end with the text FORMAT JSON' +
43+
'7. The query must not contains new lines' +
44+
'The schema of all data sources is as follows: event_time DATETIME, event_type STRING, event JSON.' +
45+
'The event column contains a JSON object using the ClickHouse JSON type. The fields should be accessed using dot notation, e.g. event.data.field_name.',
46+
parameters: z.object({
47+
// token: z.string().describe('Tinybird Admin Token used to authenticate calls to the Tinybird API'),
48+
query: z.string().describe('The SQL query to execute'),
49+
response: z.string().describe('An analysis of the data returned by the query which is shown to the user. The analysis must include the result of the query at the start.'),
50+
}),
51+
execute: async ({ query }) => {
52+
const response = await fetch(`/api/query?token=${token}&query=${encodeURIComponent(query)}`, {
53+
method: 'GET',
54+
});
55+
const data = await response.json();
56+
return data;
57+
},
58+
}),
59+
},
60+
system: 'You are the founder of a SaaS business. ' +
61+
'You want to understand your customers and their usage of your SaaS. ' +
62+
'Your product is built using various other SaaS products as building blocks. ' +
63+
'You have configured those SaaS products to push data to Tinybird. ' +
64+
'Tinybird is a data platform that allows you to query that data using SQL.' +
65+
'You can use the getAvailableDataSources tool to get the names of the data sources that you have available. ' +
66+
'You can use the queryDataSource tool to query the data sources that you have available. ',
1267
messages,
1368
});
1469

apps/web/src/app/chat/page.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
'use client';
22

33
import { useChat } from 'ai/react';
4+
import { useQueryState } from 'nuqs';
45

56
export default function Chat() {
67
const { messages, input, handleInputChange, handleSubmit } = useChat();
8+
const [token] = useQueryState('token');
79
return (
810
<div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
911
{messages.map(m => (
1012
<div key={m.id} className="whitespace-pre-wrap">
1113
{m.role === 'user' ? 'User: ' : 'AI: '}
12-
{m.content}
14+
{m.toolInvocations ? (
15+
<pre>{JSON.stringify(m.toolInvocations, null, 2)}</pre>
16+
) : (
17+
<p>{m.content}</p>
18+
)}
1319
</div>
1420
))}
1521

16-
<form onSubmit={handleSubmit}>
22+
<form onSubmit={(e) => {
23+
handleSubmit(e, { headers: { token: token ?? '' } });
24+
}}>
1725
<input
1826
className="fixed bottom-0 w-full max-w-md p-2 mb-8 border border-gray-300 rounded shadow-xl"
1927
value={input}

apps/web/src/lib/tinybird.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface TinybirdDataSource {
1919
}
2020

2121
export async function listDataSources(token: string): Promise<TinybirdDataSource[]> {
22-
const response = await fetch(`/api/datasources?token=${token}`);
22+
const response = await fetch(`http://localhost:3000/api/datasources?token=${token}`);
2323

2424
if (!response.ok) {
2525
if (response.status === 401 || response.status === 403) {
@@ -47,7 +47,7 @@ export interface QueryResult {
4747
}
4848

4949
export async function query(token: string, sql: string): Promise<QueryResult> {
50-
const response = await fetch(`/api/query?token=${token}&query=${encodeURIComponent(sql)}`);
50+
const response = await fetch(`http://localhost:3000/api/query?token=${token}&query=${encodeURIComponent(sql)}`);
5151

5252
if (!response.ok) {
5353
if (response.status === 401 || response.status === 403) {
@@ -65,7 +65,7 @@ export async function checkToolState(token: string, datasource: string): Promise
6565
// First check if data source exists
6666
const sources = await listDataSources(token);
6767
const exists = sources.some(source => source.name === datasource);
68-
68+
6969
if (!exists) {
7070
return 'available';
7171
}
@@ -84,20 +84,46 @@ export async function checkToolState(token: string, datasource: string): Promise
8484
}
8585
}
8686

87+
export async function getInstalledDataSources(token: string): Promise<string[]> {
88+
try {
89+
const sources = await listDataSources(token);
90+
const installedSources: string[] = [];
91+
92+
for (const source of sources) {
93+
try {
94+
const result = await query(token, `SELECT count(*) as count FROM ${source.name} FORMAT JSON`);
95+
if (result.data[0]?.count > 0) {
96+
installedSources.push(source.name);
97+
}
98+
} catch (error) {
99+
continue;
100+
}
101+
}
102+
103+
return installedSources;
104+
} catch (error) {
105+
if (error instanceof InvalidTokenError) {
106+
throw error;
107+
}
108+
console.error('Error getting installed data sources:', error);
109+
return [];
110+
}
111+
}
112+
87113
export async function pipe<T = QueryResult>(
88-
token: string,
89-
pipeName: string,
114+
token: string,
115+
pipeName: string,
90116
params?: Record<string, string | number | boolean>
91117
): Promise<T> {
92118
const searchParams = new URLSearchParams({ token, pipe: pipeName });
93-
119+
94120
if (params) {
95121
Object.entries(params).forEach(([key, value]) => {
96122
searchParams.append(key, String(value));
97123
});
98124
}
99125

100-
const response = await fetch(`/api/pipes?${searchParams}`);
126+
const response = await fetch(`http://localhost:3000/api/pipes?${searchParams}`);
101127

102128
if (!response.ok) {
103129
if (response.status === 401 || response.status === 403) {

0 commit comments

Comments
 (0)