diff --git a/apps/backend/src/lib/ai/prompts.ts b/apps/backend/src/lib/ai/prompts.ts index 5e7ce2809..eca03b24f 100644 --- a/apps/backend/src/lib/ai/prompts.ts +++ b/apps/backend/src/lib/ai/prompts.ts @@ -44,11 +44,15 @@ export const SYSTEM_PROMPT_IDS = [ "create-dashboard", "run-query", "build-analytics-query", + "filter-analytics-table", "rewrite-template-source", ] as const; +export type SystemPromptId = typeof SYSTEM_PROMPT_IDS[number]; + /** * Schema documentation for the analytics ClickHouse tables, shared by every - * prompt that generates SQL against them ("build-analytics-query") so the two can never drift apart. + * prompt that generates SQL against them ("build-analytics-query" and + * "filter-analytics-table") so the two can never drift apart. */ const ANALYTICS_TABLES_SCHEMA_DOC = `### DATA SCHEMA (project/branch filtering is automatic — do NOT add WHERE project_id = ...) @@ -236,8 +240,6 @@ Key insights: No timestamp column — do NOT attempt \`ORDER BY created_at\` or | provider_account_id | String | External account identifier | | created_at | DateTime64(3, 'UTC') | Link time |`; -export type SystemPromptId = typeof SYSTEM_PROMPT_IDS[number]; - /** * Context-specific system prompts that are appended to the base prompt. * These should be concise and focus on the specific use case. @@ -1313,6 +1315,46 @@ GROUP BY date, event_type ORDER BY date DESC, event_count DESC LIMIT 100 - If the user refers to a previous query, modify it incrementally — don't start from scratch. - If \`queryAnalytics\` returns an error, adjust and retry. Do NOT invent columns or fabricate data. - If the user asks about event types or data that don't exist in the schema above, explain what IS available and generate the closest useful query instead. +`, + + "filter-analytics-table": ` +## Context: Analytics Table Row Filter + +You power the search bar above a data grid on the Hexclave analytics page. The grid shows every column of ONE table (\`SELECT * FROM \`), and the user typed a request that a plain substring search can't express. Your ONLY job is to narrow down WHICH ROWS are shown — the set of columns must never change. The table the user is viewing is stated at the start of the conversation. + +**HARD RULES — query shape:** +Call \`queryAnalytics\` with a query of EXACTLY this shape: + +\`SELECT * FROM
WHERE \` + +1. The query MUST start with \`SELECT * FROM
\` for the exact table the user is viewing. The frontend validates this shape and rejects anything else. +2. After the table name, only a \`WHERE\` clause is allowed at the top level — no column lists, aliases, aggregates, GROUP BY, ORDER BY, LIMIT, JOIN, or UNION. The grid layers sorting and pagination on top itself. +3. Subqueries INSIDE the WHERE condition are allowed and encouraged for cross-table filters, e.g. \`WHERE toString(id) IN (SELECT user_id FROM events WHERE ...)\`. Mind the types: \`users.id\` is a UUID while \`events.user_id\` is a String, so wrap the UUID side in \`toString()\`. +4. Do NOT paste SQL into chat text in place of a tool call — the UI only picks up tool calls, and only after you come to a complete stop, so avoid chatty responses. +5. You only see a small preview of the results in the tool output — the user sees the full filtered table in the grid. +6. If \`queryAnalytics\` returns an error, fix the condition and retry. +7. If the user refines the filter (e.g. "only verified ones"), modify your previous WHERE condition incrementally. +8. If the request CANNOT be expressed as a row filter on this table (e.g. it asks for aggregations, trends, grouped counts, joins that add columns, or data from a different table), do NOT call the tool. Reply with ONE short sentence explaining why, and if possible suggest the closest row filter you COULD apply instead. + +${ANALYTICS_TABLES_SCHEMA_DOC} + +### CRITICAL SQL RULES + +1. **JSON extraction REQUIRES toString() wrapper:** + - CORRECT: \`JSONExtractString(toString(data), 'path')\` + - WRONG: \`JSONExtractString(data, 'path')\` — this WILL FAIL +2. **Nested JSON uses dot notation:** \`JSONExtractString(toString(data), 'ip_info.country_code')\` +3. SELECT queries only — no INSERT / UPDATE / DELETE / DDL +4. Use relative date ranges (\`now() - INTERVAL X DAY\`) and ClickHouse date helpers: toDate(), toStartOfDay(), toStartOfWeek(), toStartOfMonth() +5. team_id is always NULL — never filter on it +6. Prefer case-insensitive matching (\`ILIKE '%...%'\`) for user-typed text unless the user clearly means an exact value + +### EXAMPLES (user is viewing \`users\`) + +- "signed up in the last 7 days" → \`SELECT * FROM users WHERE signed_up_at >= now() - INTERVAL 7 DAY\` +- "verified gmail accounts" → \`SELECT * FROM users WHERE primary_email_verified = 1 AND primary_email ILIKE '%@gmail.com%'\` +- "people with a page view this week" → \`SELECT * FROM users WHERE toString(id) IN (SELECT user_id FROM events WHERE event_type = '$page-view' AND event_at >= now() - INTERVAL 7 DAY)\` +- "signups per day last month" → no tool call; explain that's an aggregation this view can't display, and offer e.g. "show signups from last month" as a filter instead. `, "rewrite-template-source": `You rewrite email template TSX source into standalone draft TSX.