Skip to content

Commit cb01751

Browse files
committed
execute query builder tool for agent
1 parent 2b1c10d commit cb01751

File tree

5 files changed

+146
-4
lines changed

5 files changed

+146
-4
lines changed

apps/api/src/ai/agents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { models } from "./config/models";
1111
import { buildAnalyticsInstructions } from "./prompts/analytics";
1212
import { buildReflectionInstructions } from "./prompts/reflection";
1313
import { buildTriageInstructions } from "./prompts/triage";
14+
import { executeQueryBuilderTool } from "./tools/execute-query-builder";
1415
import { executeSqlQueryTool } from "./tools/execute-sql-query";
1516
import { getTopPagesTool } from "./tools/get-top-pages";
1617
import { competitorAnalysisTool, webSearchTool } from "./tools/web-search";
@@ -20,6 +21,7 @@ import { competitorAnalysisTool, webSearchTool } from "./tools/web-search";
2021
*/
2122
const analyticsTools = {
2223
get_top_pages: getTopPagesTool,
24+
execute_query_builder: executeQueryBuilderTool,
2325
execute_sql_query: executeSqlQueryTool,
2426
web_search: webSearchTool,
2527
competitor_analysis: competitorAnalysisTool,

apps/api/src/ai/config/context.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export type AppContext = {
1010
currentDateTime: string;
1111
chatId: string;
1212
requestHeaders?: Headers;
13+
/** Available query builder types */
14+
availableQueryTypes?: string[];
1315
[key: string]: unknown;
1416
};
1517

@@ -37,14 +39,20 @@ export function buildAppContext(
3739
* This provides structured data the LLM can reference.
3840
*/
3941
export function formatContextForLLM(context: AppContext): string {
42+
const queryTypesInfo = context.availableQueryTypes
43+
? `\n<available_query_types>${context.availableQueryTypes.join(", ")}</available_query_types>`
44+
: "";
45+
4046
return `<website_info>
4147
<current_date>${context.currentDateTime}</current_date>
4248
<timezone>${context.timezone}</timezone>
4349
<website_id>${context.websiteId}</website_id>
44-
<website_domain>${context.websiteDomain}</website_domain>
50+
<website_domain>${context.websiteDomain}</website_domain>${queryTypesInfo}
4551
</website_info>
4652
4753
IMPORTANT CONTEXT VALUES:
4854
- Use current_date for time-sensitive operations
49-
- Use website_id value "${context.websiteId}" when filtering by client_id in SQL queries (pass as params.websiteId)`;
55+
- Use website_id value "${context.websiteId}" when filtering by client_id in SQL queries (pass as params.websiteId)
56+
- Use execute_query_builder tool for pre-built analytics queries (preferred over custom SQL)
57+
- Use execute_sql_query tool only when you need custom SQL that isn't covered by query builders`;
5058
}

apps/api/src/ai/prompts/analytics.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ const ANALYTICS_RULES = `<agent-specific-rules>
1717
1818
**Tools Usage:**
1919
- Use get_top_pages for page analytics
20-
- Use execute_sql_query for custom analytics queries
20+
- Use execute_query_builder for pre-built analytics queries (PREFERRED - use this for common queries like traffic, sessions, pages, devices, geo, errors, performance, etc.)
21+
- Use execute_sql_query ONLY for custom SQL queries that aren't covered by query builders
2122
- Use competitor_analysis for real-time competitor insights, market trends, and industry analysis with citations
2223
- Use memory tools (search_memory, add_memory) to remember user preferences and past analysis patterns
2324
- CRITICAL: execute_sql_query must ONLY use SELECT/WITH and parameter placeholders (e.g., {limit:UInt32}) with values passed via params. websiteId is automatically included. Never interpolate strings.
24-
- Example: execute_sql_query({ websiteId: "<use website_id from context>", sql: "SELECT ... WHERE client_id = {websiteId:String}", params: { limit: 10 } })
25+
- Example query builder: execute_query_builder({ websiteId: "<use website_id from context>", type: "traffic", from: "2024-01-01", to: "2024-01-31", timeUnit: "day" })
26+
- Example custom SQL: execute_sql_query({ websiteId: "<use website_id from context>", sql: "SELECT ... WHERE client_id = {websiteId:String}", params: { limit: 10 } })
2527
- Example competitor analysis: competitor_analysis({ query: "competitors to example.com in web analytics", context: "Our website tracks user behavior and performance metrics" })
2628
- Example memory: search_memory({ query: "user's preferred metrics" })
2729
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { tool } from "ai";
2+
import { z } from "zod";
3+
import { getWebsiteDomain } from "../../lib/website-utils";
4+
import { executeQuery, QueryBuilders } from "../../query";
5+
import { createToolLogger } from "./utils/logger";
6+
import type { QueryRequest } from "../../query/types";
7+
8+
const QueryBuilderInputSchema = z.object({
9+
websiteId: z.string().describe("The website ID to query"),
10+
type: z
11+
.string()
12+
.describe(
13+
`The query type to execute. Available types: ${Object.keys(QueryBuilders).join(", ")}`
14+
),
15+
from: z.string().describe("Start date in ISO format (e.g., 2024-01-01)"),
16+
to: z.string().describe("End date in ISO format (e.g., 2024-01-31)"),
17+
timeUnit: z
18+
.enum(["minute", "hour", "day", "week", "month"])
19+
.optional()
20+
.describe("Time granularity for the query"),
21+
filters: z
22+
.array(
23+
z.object({
24+
field: z.string(),
25+
op: z.enum(["eq", "ne", "contains", "not_contains", "starts_with", "in", "not_in"]),
26+
value: z.union([z.string(), z.number(), z.array(z.union([z.string(), z.number()]))]),
27+
target: z.string().optional(),
28+
having: z.boolean().optional(),
29+
})
30+
)
31+
.optional()
32+
.describe("Filters to apply to the query"),
33+
groupBy: z.array(z.string()).optional().describe("Fields to group by"),
34+
orderBy: z.string().optional().describe("Field to order by"),
35+
limit: z.number().min(1).max(1000).optional().describe("Maximum number of results"),
36+
offset: z.number().min(0).optional().describe("Number of results to skip"),
37+
timezone: z.string().optional().describe("Timezone for date operations (default: UTC)"),
38+
websiteDomain: z
39+
.string()
40+
.optional()
41+
.describe("Website domain (optional, will be fetched if not provided)"),
42+
});
43+
44+
type QueryBuilderInput = z.infer<typeof QueryBuilderInputSchema>;
45+
46+
/**
47+
* Tool for executing pre-built queries using the query builder system.
48+
* This provides a safe, structured way to query analytics data without writing raw SQL.
49+
* Use this for common analytics queries like page views, sessions, traffic, etc.
50+
* For custom queries, use execute_sql_query instead.
51+
*/
52+
export const executeQueryBuilderTool = tool({
53+
description: `Executes a pre-built analytics query using the query builder system. Available query types: ${Object.keys(QueryBuilders).join(", ")}. This is the preferred method for common analytics queries as it provides type safety and automatic optimization. Use execute_sql_query only when you need custom SQL that isn't covered by these builders.`,
54+
inputSchema: QueryBuilderInputSchema,
55+
execute: async (input: QueryBuilderInput): Promise<{
56+
data: unknown[];
57+
executionTime: number;
58+
rowCount: number;
59+
type: string;
60+
}> => {
61+
const logger = createToolLogger("Execute Query Builder");
62+
const queryStart = Date.now();
63+
64+
try {
65+
// Validate query type exists
66+
if (!QueryBuilders[input.type]) {
67+
throw new Error(
68+
`Unknown query type: ${input.type}. Available types: ${Object.keys(QueryBuilders).join(", ")}`
69+
);
70+
}
71+
72+
// Fetch website domain if not provided
73+
const websiteDomain =
74+
input.websiteDomain ?? (await getWebsiteDomain(input.websiteId));
75+
76+
// Build query request
77+
const queryRequest: QueryRequest = {
78+
projectId: input.websiteId,
79+
type: input.type,
80+
from: input.from,
81+
to: input.to,
82+
timeUnit: input.timeUnit,
83+
filters: input.filters,
84+
groupBy: input.groupBy,
85+
orderBy: input.orderBy,
86+
limit: input.limit,
87+
offset: input.offset,
88+
timezone: input.timezone ?? "UTC",
89+
};
90+
91+
// Execute query
92+
const data = await executeQuery(
93+
queryRequest,
94+
websiteDomain,
95+
queryRequest.timezone
96+
);
97+
98+
const executionTime = Date.now() - queryStart;
99+
100+
logger.info("Query builder executed", {
101+
type: input.type,
102+
executionTime: `${executionTime}ms`,
103+
rowCount: data.length,
104+
from: input.from,
105+
to: input.to,
106+
});
107+
108+
return {
109+
data,
110+
executionTime,
111+
rowCount: data.length,
112+
type: input.type,
113+
};
114+
} catch (error) {
115+
const executionTime = Date.now() - queryStart;
116+
117+
logger.error("Query builder failed", {
118+
type: input.type,
119+
executionTime: `${executionTime}ms`,
120+
error: error instanceof Error ? error.message : "Unknown error",
121+
});
122+
123+
throw error instanceof Error
124+
? error
125+
: new Error("Query builder execution failed");
126+
}
127+
},
128+
});

apps/api/src/routes/agent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { createReflectionAgent, createTriageAgent } from "../ai/agents";
55
import { buildAppContext } from "../ai/config/context";
66
import { record, setAttributes } from "../lib/tracing";
77
import { validateWebsite } from "../lib/website-utils";
8+
import { QueryBuilders } from "../query/builders";
89

910
const AgentRequestSchema = t.Object({
1011
websiteId: t.String(),
@@ -135,6 +136,7 @@ export const agent = new Elysia({ prefix: "/v1/agent" })
135136
...appContext,
136137
chatId,
137138
requestHeaders: request.headers,
139+
availableQueryTypes: Object.keys(QueryBuilders),
138140
};
139141

140142
// Select agent based on model preference

0 commit comments

Comments
 (0)