mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
feat: replace hardcoded schema in AI prompts with SHOW TABLES/DESCRIBE TABLE discovery (#1706)
This commit is contained in:
parent
30f361f0a6
commit
0814755e88
@ -68,6 +68,17 @@ export async function runClickhouseMigrations() {
|
||||
client.command({ query: SIGN_UP_RULE_TRIGGER_EVENT_ROW_FORMAT_MUTATION_SQL }),
|
||||
]);
|
||||
|
||||
// Add column comments to all views so DESCRIBE TABLE returns useful descriptions.
|
||||
// Comments are lost on CREATE OR REPLACE VIEW, so we re-apply them every migration run.
|
||||
// Using allSettled so a stale comment (e.g. renamed column) doesn't break startup.
|
||||
const commentResults = await Promise.allSettled(COLUMN_COMMENTS.map(sql =>
|
||||
client.command({ query: sql })
|
||||
));
|
||||
const commentFailures = commentResults.filter(r => r.status === "rejected");
|
||||
if (commentFailures.length > 0) {
|
||||
console.warn(`[clickhouse-migrations] ${commentFailures.length}/${COLUMN_COMMENTS.length} column comment(s) failed to apply`);
|
||||
}
|
||||
|
||||
// Row policies in parallel
|
||||
const tables = [
|
||||
"events", "users", "contact_channels", "teams", "team_member_profiles",
|
||||
@ -652,6 +663,160 @@ FINAL
|
||||
WHERE sync_is_deleted = 0;
|
||||
`;
|
||||
|
||||
// ─── Column comments ────────────────────────────────────────────────
|
||||
// Applied to the default.* views after creation so that DESCRIBE TABLE
|
||||
// returns useful descriptions for each column. The AI assistant uses
|
||||
// SHOW TABLES + DESCRIBE TABLE for schema discovery instead of
|
||||
// hardcoded schema in the prompt.
|
||||
const COLUMN_COMMENTS: string[] = [
|
||||
// ── events ──
|
||||
`ALTER TABLE default.events COMMENT COLUMN event_type 'Event type identifier. Known types: \$page-view, \$click, \$token-refresh, \$sign-up-rule-trigger'`,
|
||||
`ALTER TABLE default.events COMMENT COLUMN event_at 'When the event occurred (UTC)'`,
|
||||
`ALTER TABLE default.events COMMENT COLUMN data 'Event payload as JSON. MUST use toString(data) before JSONExtract* functions. Payload varies by event_type: \$page-view → {is_anonymous, path, referrer}; \$click → {is_anonymous, selector, url, viewport_width, viewport_height, x, y, ...}; \$token-refresh → {is_anonymous, refresh_token_id, ip_info: {country_code, city_name, region_code, is_trusted, latitude, longitude, tz_identifier, ip}}'`,
|
||||
`ALTER TABLE default.events COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.events COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.events COMMENT COLUMN user_id 'User who triggered the event. Always populated despite Nullable type'`,
|
||||
`ALTER TABLE default.events COMMENT COLUMN team_id 'Reserved for future use. Currently always NULL — do not filter on this column'`,
|
||||
`ALTER TABLE default.events COMMENT COLUMN created_at 'When this record was inserted into the database (UTC)'`,
|
||||
`ALTER TABLE default.events COMMENT COLUMN refresh_token_id 'Denormalized from data.refresh_token_id for \$token-refresh events. NULL for other event types'`,
|
||||
`ALTER TABLE default.events COMMENT COLUMN session_replay_id 'Session replay identifier for linking to replay recordings'`,
|
||||
`ALTER TABLE default.events COMMENT COLUMN session_replay_segment_id 'Segment within a session replay recording'`,
|
||||
|
||||
// ── users ──
|
||||
`ALTER TABLE default.users COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN id 'Unique user identifier (UUID primary key)'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN display_name 'User-facing display name set by the user or application'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN profile_image_url 'URL to the user profile/avatar image'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN primary_email 'User primary email address'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN primary_email_verified '1 if the primary email has been verified, 0 otherwise'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN signed_up_at 'When the user first signed up (UTC)'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN client_metadata 'Application-defined JSON metadata readable and writable from client SDKs'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN client_read_only_metadata 'Application-defined JSON metadata readable from client SDKs but only writable from server'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN server_metadata 'Application-defined JSON metadata only accessible from server SDKs'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN is_anonymous '1 if this is an anonymous/guest user, 0 for authenticated users'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN restricted_by_admin '1 if an admin has restricted this user access'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN restricted_by_admin_reason 'Admin-provided reason for restricting the user, shown to the user'`,
|
||||
`ALTER TABLE default.users COMMENT COLUMN restricted_by_admin_private_details 'Private admin notes about the restriction, not shown to the user'`,
|
||||
|
||||
// ── contact_channels ──
|
||||
`ALTER TABLE default.contact_channels COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.contact_channels COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.contact_channels COMMENT COLUMN id 'Unique contact channel identifier'`,
|
||||
`ALTER TABLE default.contact_channels COMMENT COLUMN user_id 'Owner user ID (join to users.id)'`,
|
||||
`ALTER TABLE default.contact_channels COMMENT COLUMN type 'Channel type, e.g. email'`,
|
||||
`ALTER TABLE default.contact_channels COMMENT COLUMN value 'Channel value, e.g. the email address'`,
|
||||
`ALTER TABLE default.contact_channels COMMENT COLUMN is_primary '1 if this is the user primary contact channel'`,
|
||||
`ALTER TABLE default.contact_channels COMMENT COLUMN is_verified '1 if ownership of this channel has been verified'`,
|
||||
`ALTER TABLE default.contact_channels COMMENT COLUMN used_for_auth '1 if this channel can be used as an authentication identifier'`,
|
||||
`ALTER TABLE default.contact_channels COMMENT COLUMN created_at 'When this contact channel was created (UTC)'`,
|
||||
|
||||
// ── teams ──
|
||||
`ALTER TABLE default.teams COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.teams COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.teams COMMENT COLUMN id 'Unique team identifier'`,
|
||||
`ALTER TABLE default.teams COMMENT COLUMN display_name 'Team name shown in the UI'`,
|
||||
`ALTER TABLE default.teams COMMENT COLUMN profile_image_url 'URL to the team logo/avatar image'`,
|
||||
`ALTER TABLE default.teams COMMENT COLUMN created_at 'When the team was created (UTC)'`,
|
||||
`ALTER TABLE default.teams COMMENT COLUMN client_metadata 'Application-defined JSON metadata readable and writable from client SDKs'`,
|
||||
`ALTER TABLE default.teams COMMENT COLUMN client_read_only_metadata 'Application-defined JSON metadata readable from client SDKs but only writable from server'`,
|
||||
`ALTER TABLE default.teams COMMENT COLUMN server_metadata 'Application-defined JSON metadata only accessible from server SDKs'`,
|
||||
|
||||
// ── team_member_profiles ──
|
||||
`ALTER TABLE default.team_member_profiles COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.team_member_profiles COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.team_member_profiles COMMENT COLUMN team_id 'Team this membership belongs to (join to teams.id)'`,
|
||||
`ALTER TABLE default.team_member_profiles COMMENT COLUMN user_id 'User in this membership (join to users.id)'`,
|
||||
`ALTER TABLE default.team_member_profiles COMMENT COLUMN display_name 'Per-team display name override. NULL means use the user default display_name'`,
|
||||
`ALTER TABLE default.team_member_profiles COMMENT COLUMN profile_image_url 'Per-team profile image override. NULL means use the user default'`,
|
||||
`ALTER TABLE default.team_member_profiles COMMENT COLUMN created_at 'When this team membership was created (UTC)'`,
|
||||
|
||||
// ── team_permissions ──
|
||||
`ALTER TABLE default.team_permissions COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.team_permissions COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.team_permissions COMMENT COLUMN team_id 'Team this permission is scoped to (join to teams.id)'`,
|
||||
`ALTER TABLE default.team_permissions COMMENT COLUMN user_id 'User granted this permission (join to users.id)'`,
|
||||
`ALTER TABLE default.team_permissions COMMENT COLUMN id 'Permission identifier string, e.g. admin, member'`,
|
||||
`ALTER TABLE default.team_permissions COMMENT COLUMN created_at 'When this permission was granted (UTC)'`,
|
||||
|
||||
// ── team_invitations ──
|
||||
`ALTER TABLE default.team_invitations COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.team_invitations COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.team_invitations COMMENT COLUMN id 'Unique invitation identifier'`,
|
||||
`ALTER TABLE default.team_invitations COMMENT COLUMN team_id 'Team being invited to (join to teams.id)'`,
|
||||
`ALTER TABLE default.team_invitations COMMENT COLUMN team_display_name 'Snapshot of the team name at invitation time'`,
|
||||
`ALTER TABLE default.team_invitations COMMENT COLUMN recipient_email 'Email address the invitation was sent to'`,
|
||||
`ALTER TABLE default.team_invitations COMMENT COLUMN expires_at_millis 'Invitation expiry as Unix milliseconds. Compare with toUnixTimestamp64Milli(now())'`,
|
||||
`ALTER TABLE default.team_invitations COMMENT COLUMN created_at 'When the invitation was created (UTC)'`,
|
||||
|
||||
// ── email_outboxes ──
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN id 'Unique email record identifier'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN status 'Granular delivery status from the email provider'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN simple_status 'Simplified status for reporting, e.g. sent, delivered, failed'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN created_with 'How this email was created, e.g. programmatic API or draft editor'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN email_draft_id 'ID of the email draft template used, if created from a draft'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN email_programmatic_call_template_id 'ID of the programmatic template, if sent via API'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN theme_id 'Email theme/design ID applied to this email'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN is_high_priority '1 if marked as high priority for send ordering'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN is_transactional '1 for transactional emails (e.g. verification), NULL if unknown'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN subject 'Email subject line'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN notification_category_id 'Category for notification preferences/unsubscribe'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN started_rendering_at 'When email rendering began (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN rendered_at 'When email rendering completed (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN render_error 'Error message if rendering failed. Non-null implies render failure'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN scheduled_at 'When the email is/was scheduled to be sent (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN created_at 'When this email record was created (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN updated_at 'When this email record was last updated (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN started_sending_at 'When the send attempt began (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN server_error 'Error from the email provider. Non-null implies send failure'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN delivered_at 'When the email was confirmed delivered (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN opened_at 'When the recipient first opened the email (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN clicked_at 'When the recipient first clicked a link in the email (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN unsubscribed_at 'When the recipient unsubscribed via this email (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN marked_as_spam_at 'When the recipient marked this email as spam (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN bounced_at 'When the email bounced (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN delivery_delayed_at 'When a delivery delay was reported (UTC)'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN can_have_delivery_info '1 if the email provider supports delivery tracking for this email'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN skipped_reason 'Why sending was skipped, if applicable. Non-null implies send was skipped'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN skipped_details 'Additional details about why sending was skipped'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN send_retries 'Number of send retry attempts made'`,
|
||||
`ALTER TABLE default.email_outboxes COMMENT COLUMN is_paused '1 if email sending is currently paused'`,
|
||||
|
||||
// ── project_permissions ──
|
||||
`ALTER TABLE default.project_permissions COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.project_permissions COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.project_permissions COMMENT COLUMN user_id 'User granted this permission (join to users.id)'`,
|
||||
`ALTER TABLE default.project_permissions COMMENT COLUMN id 'Permission identifier string'`,
|
||||
`ALTER TABLE default.project_permissions COMMENT COLUMN created_at 'When this permission was granted (UTC)'`,
|
||||
|
||||
// ── notification_preferences ──
|
||||
`ALTER TABLE default.notification_preferences COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.notification_preferences COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.notification_preferences COMMENT COLUMN user_id 'User these preferences belong to (join to users.id)'`,
|
||||
`ALTER TABLE default.notification_preferences COMMENT COLUMN notification_category_id 'Notification category this preference applies to'`,
|
||||
`ALTER TABLE default.notification_preferences COMMENT COLUMN enabled '1 if the user has opted in to this notification category, 0 if opted out'`,
|
||||
|
||||
// ── refresh_tokens ──
|
||||
`ALTER TABLE default.refresh_tokens COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.refresh_tokens COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.refresh_tokens COMMENT COLUMN id 'Unique token identifier'`,
|
||||
`ALTER TABLE default.refresh_tokens COMMENT COLUMN user_id 'User this token belongs to (join to users.id)'`,
|
||||
`ALTER TABLE default.refresh_tokens COMMENT COLUMN created_at 'When the token was issued (UTC)'`,
|
||||
`ALTER TABLE default.refresh_tokens COMMENT COLUMN last_used_at 'When the token was last exchanged for an access token (UTC). Proxy for session activity'`,
|
||||
`ALTER TABLE default.refresh_tokens COMMENT COLUMN is_impersonation '1 if this is a dashboard/admin impersonation session'`,
|
||||
`ALTER TABLE default.refresh_tokens COMMENT COLUMN expires_at 'When the token expires (UTC). NULL means non-expiring'`,
|
||||
|
||||
// ── connected_accounts ──
|
||||
`ALTER TABLE default.connected_accounts COMMENT COLUMN project_id 'Project identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.connected_accounts COMMENT COLUMN branch_id 'Branch identifier. Auto-filtered by row-level security — do not use in WHERE clauses'`,
|
||||
`ALTER TABLE default.connected_accounts COMMENT COLUMN user_id 'User this account is linked to (join to users.id)'`,
|
||||
`ALTER TABLE default.connected_accounts COMMENT COLUMN provider 'OAuth/SSO provider name, e.g. google, github'`,
|
||||
`ALTER TABLE default.connected_accounts COMMENT COLUMN provider_account_id 'User account ID at the external provider'`,
|
||||
`ALTER TABLE default.connected_accounts COMMENT COLUMN created_at 'When this account was linked (UTC)'`,
|
||||
];
|
||||
|
||||
const EXTERNAL_ANALYTICS_DB_SQL = `
|
||||
CREATE DATABASE IF NOT EXISTS analytics_internal;
|
||||
`;
|
||||
|
||||
@ -77,31 +77,10 @@ You are a Hexclave assistant in a dashboard search bar.
|
||||
|
||||
Run a ClickHouse SQL query against the project's analytics database. Only SELECT queries are allowed. Project filtering is automatic - you don't need WHERE project_id = ...
|
||||
|
||||
Available tables:
|
||||
|
||||
**events** - User activity events
|
||||
- event_type: LowCardinality(String) - ONLY: $page-view, $click, $token-refresh
|
||||
- event_at: DateTime64(3, 'UTC') - When the event occurred
|
||||
- data: JSON - MUST use toString() before extracting: JSONExtractString(toString(data), 'key')
|
||||
- user_id: Nullable(String) - Always populated (no nulls)
|
||||
- team_id: Nullable(String) - Always NULL, never use
|
||||
- created_at: DateTime64(3, 'UTC') - When the record was created
|
||||
|
||||
Event data payloads:
|
||||
- $page-view: {is_anonymous, path, referrer}
|
||||
- $click: {is_anonymous, selector}
|
||||
- $token-refresh: {is_anonymous, refresh_token_id, ip_info: {country_code, city_name, region_code, is_trusted, latitude, longitude, tz_identifier, ip}}
|
||||
|
||||
**users** - User profiles
|
||||
- id: UUID - User ID
|
||||
- display_name: Nullable(String) - User's display name
|
||||
- primary_email: Nullable(String) - User's primary email
|
||||
- primary_email_verified: UInt8 - Whether email is verified (0/1)
|
||||
- signed_up_at: DateTime64(3, 'UTC') - When user signed up
|
||||
- client_metadata: JSON - Typically empty
|
||||
- client_read_only_metadata: JSON - Typically empty
|
||||
- server_metadata: JSON - Typically empty
|
||||
- is_anonymous: UInt8 - Whether user is anonymous (0/1)
|
||||
SCHEMA DISCOVERY:
|
||||
- Use \`SHOW TABLES\` to list available tables
|
||||
- Use \`DESCRIBE TABLE <table_name>\` to see columns, types, and descriptions for any table
|
||||
- Column comments contain important constraints and usage notes — always read them before querying a table for the first time
|
||||
|
||||
SQL QUERY GUIDELINES:
|
||||
- Only SELECT queries are allowed (no INSERT, UPDATE, DELETE)
|
||||
@ -763,26 +742,8 @@ Two ways to use ClickHouse:
|
||||
|
||||
Project + branch filtering is AUTOMATIC in both cases. Do NOT add \`WHERE project_id = ...\`.
|
||||
|
||||
Available tables (same schema in both contexts):
|
||||
|
||||
events:
|
||||
- event_type: LowCardinality(String) ($token-refresh only, today)
|
||||
- event_at: DateTime64(3, 'UTC')
|
||||
- data: JSON
|
||||
- user_id: Nullable(String)
|
||||
- team_id: Nullable(String)
|
||||
- created_at: DateTime64(3, 'UTC')
|
||||
|
||||
users (limited fields):
|
||||
- id: UUID
|
||||
- display_name: Nullable(String)
|
||||
- primary_email: Nullable(String)
|
||||
- primary_email_verified: UInt8 (0/1)
|
||||
- signed_up_at: DateTime64(3, 'UTC')
|
||||
- client_metadata: JSON
|
||||
- client_read_only_metadata: JSON
|
||||
- server_metadata: JSON
|
||||
- is_anonymous: UInt8 (0/1)
|
||||
Use \`SHOW TABLES\` to list available tables and \`DESCRIBE TABLE <table_name>\` to see columns, types, and descriptions.
|
||||
Column comments explain purpose and constraints \u2014 always check them before writing queries.
|
||||
|
||||
────────────────────────────────────────
|
||||
INSPECTION LOOP — USE SPARINGLY
|
||||
@ -999,31 +960,10 @@ You MUST call the updateDashboard tool with the complete source code. NEVER outp
|
||||
|
||||
You are helping users query their Hexclave project's analytics data using ClickHouse SQL.
|
||||
|
||||
**Available Tables:**
|
||||
|
||||
**events** - User activity events
|
||||
- event_type: LowCardinality(String) - ONLY: $page-view, $click, $token-refresh
|
||||
- event_at: DateTime64(3, 'UTC') - When the event occurred
|
||||
- data: JSON - MUST use toString() before extracting: JSONExtractString(toString(data), 'key')
|
||||
- user_id: Nullable(String) - Always populated (no nulls)
|
||||
- team_id: Nullable(String) - Always NULL, never use
|
||||
- created_at: DateTime64(3, 'UTC') - When the record was created
|
||||
|
||||
Event data payloads:
|
||||
- $page-view: {is_anonymous, path, referrer}
|
||||
- $click: {is_anonymous, selector}
|
||||
- $token-refresh: {is_anonymous, refresh_token_id, ip_info: {country_code, city_name, region_code, is_trusted, latitude, longitude, tz_identifier, ip}}
|
||||
|
||||
**users** - User profiles
|
||||
- id: UUID - User ID
|
||||
- display_name: Nullable(String) - User's display name
|
||||
- primary_email: Nullable(String) - User's primary email
|
||||
- primary_email_verified: UInt8 - Whether email is verified (0/1)
|
||||
- signed_up_at: DateTime64(3, 'UTC') - When user signed up
|
||||
- client_metadata: JSON - Typically empty
|
||||
- client_read_only_metadata: JSON - Typically empty
|
||||
- server_metadata: JSON - Typically empty
|
||||
- is_anonymous: UInt8 - Whether user is anonymous (0/1)
|
||||
**Schema Discovery:**
|
||||
- Use \`SHOW TABLES\` to list available tables
|
||||
- Use \`DESCRIBE TABLE <table_name>\` to see columns, types, and descriptions
|
||||
- Column comments explain purpose, constraints, and valid values for each column \u2014 always check them before querying a table for the first time
|
||||
|
||||
**SQL Query Guidelines:**
|
||||
- Only SELECT queries are allowed (no INSERT, UPDATE, DELETE)
|
||||
@ -1051,7 +991,7 @@ Event data payloads:
|
||||
"build-analytics-query": `
|
||||
## Context: Analytics Query Builder
|
||||
|
||||
You are a ClickHouse SQL expert helping the user build queries that drive a data grid on the Hexclave analytics page. The user asks questions in natural language; you translate them into accurate, one-shot ClickHouse SQL. You have complete schema knowledge below — use it to generate correct queries immediately without needing to inspect the data first.
|
||||
You are a ClickHouse SQL expert helping the user build queries that drive a data grid on the Hexclave analytics page. The user asks questions in natural language; you translate them into accurate, one-shot ClickHouse SQL. Always run DESCRIBE TABLE on relevant tables before writing queries — column comments contain the authoritative constraints and valid values you need to generate correct queries.
|
||||
|
||||
**HARD RULE — how the tool works:**
|
||||
Call \`queryAnalytics\` with your SQL query. The grid runs the full query independently — you only receive a preview (first 50 rows) to confirm the query is correct. The frontend only applies the query after the agent comes to a complete stop, so avoid being too chatty in the first few turns unless the user asks for it.
|
||||
@ -1060,191 +1000,10 @@ Call \`queryAnalytics\` with your SQL query. The grid runs the full query indepe
|
||||
3. Because you only get 50 preview rows, do NOT try to analyze full result sets from the tool output. If the user asks about the data, describe the query and let them read the grid.
|
||||
4. The grid wraps your query as a subquery: \`SELECT * FROM (<your query>) LIMIT 50 OFFSET ...\` and paginates via infinite scroll. Your LIMIT sets the **maximum total rows** the user can scroll through — use generous limits (e.g. 1000 for aggregates) so the grid can paginate the full result.
|
||||
|
||||
### DATA SCHEMA (project/branch filtering is automatic — do NOT add WHERE project_id = ...)
|
||||
### SCHEMA DISCOVERY (project/branch filtering is automatic — do NOT add WHERE project_id = ...)
|
||||
|
||||
**users** table:
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| id | UUID | Primary key |
|
||||
| display_name | Nullable(String) | Typically populated |
|
||||
| primary_email | Nullable(String) | Usually present |
|
||||
| primary_email_verified | UInt8 (0/1) | Primary user segmentation axis |
|
||||
| signed_up_at | DateTime64(3, 'UTC') | High-resolution timestamp |
|
||||
| is_anonymous | UInt8 (0/1) | Rare; mostly testing |
|
||||
| client_metadata | JSON | Typically empty {} |
|
||||
| server_metadata | JSON | Typically empty {} |
|
||||
| client_read_only_metadata | JSON | Typically empty {} |
|
||||
| restricted_by_admin | UInt8 (0/1) | Rare; administrative flag |
|
||||
|
||||
Key insights: Metadata fields are sparse/empty — don't expect rich structures. Email verification is the primary segmentation. Anonymous users are negligible.
|
||||
|
||||
**events** table:
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| event_type | LowCardinality(String) | ONLY: \`$page-view\`, \`$click\`, \`$token-refresh\` |
|
||||
| event_at | DateTime64(3, 'UTC') | Use for aggregation by day/week/month |
|
||||
| data | JSON | Native JSON — MUST use toString() before extracting (see rules) |
|
||||
| user_id | Nullable(String) | 100% populated (no nulls); safe for filtering/joins |
|
||||
| team_id | Nullable(String) | Always NULL — never use it |
|
||||
| created_at | DateTime64(3, 'UTC') | Processing timestamp |
|
||||
|
||||
### JSON PAYLOAD STRUCTURES (per event_type)
|
||||
|
||||
**\`$page-view\`** data:
|
||||
\`\`\`json
|
||||
{"is_anonymous": false, "path": "/some-page", "referrer": "http://...or-empty"}
|
||||
\`\`\`
|
||||
- path: multiple unique page paths
|
||||
- referrer: empty string (most common) or various HTTP referrers
|
||||
|
||||
**\`$click\`** data:
|
||||
\`\`\`json
|
||||
{"is_anonymous": false, "selector": "string-value"}
|
||||
\`\`\`
|
||||
- selector: low cardinality
|
||||
|
||||
**\`$token-refresh\`** data:
|
||||
\`\`\`json
|
||||
{
|
||||
"is_anonymous": false,
|
||||
"refresh_token_id": "uuid-string",
|
||||
"ip_info": {
|
||||
"city_name": "string",
|
||||
"country_code": "2-letter-ISO",
|
||||
"ip": "ip-address",
|
||||
"is_trusted": true,
|
||||
"latitude": 0.0,
|
||||
"longitude": 0.0,
|
||||
"region_code": "string",
|
||||
"tz_identifier": "timezone-string"
|
||||
}
|
||||
}
|
||||
\`\`\`
|
||||
- Token refresh is an excellent proxy for active authenticated sessions
|
||||
- ip_info has rich geolocation data for geo-based analysis
|
||||
|
||||
### OTHER TABLES
|
||||
|
||||
**contact_channels** table (email/phone channels attached to a user):
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| id | UUID | Primary key |
|
||||
| user_id | UUID | Join to users.id |
|
||||
| type | LowCardinality(String) | Channel type, e.g. \`email\` |
|
||||
| value | String | The channel value (e.g. email address) |
|
||||
| is_primary | UInt8 (0/1) | Primary contact for the user |
|
||||
| is_verified | UInt8 (0/1) | Verification status |
|
||||
| used_for_auth | UInt8 (0/1) | Usable as an auth identifier |
|
||||
| created_at | DateTime64(3, 'UTC') | |
|
||||
|
||||
**teams** table:
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| id | UUID | Primary key |
|
||||
| display_name | String | |
|
||||
| profile_image_url | Nullable(String) | |
|
||||
| created_at | DateTime64(3, 'UTC') | |
|
||||
| client_metadata | String | JSON string; typically empty |
|
||||
| client_read_only_metadata | String | JSON string; typically empty |
|
||||
| server_metadata | String | JSON string; typically empty |
|
||||
|
||||
**team_member_profiles** table (team membership + per-team profile overrides — this is the join table of users ↔ teams):
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| team_id | UUID | Join to teams.id |
|
||||
| user_id | UUID | Join to users.id |
|
||||
| display_name | Nullable(String) | Per-team override |
|
||||
| profile_image_url | Nullable(String) | Per-team override |
|
||||
| created_at | DateTime64(3, 'UTC') | Membership creation |
|
||||
|
||||
**team_permissions** table (permissions granted to a user inside a specific team):
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| team_id | UUID | |
|
||||
| user_id | UUID | |
|
||||
| id | String | Permission identifier (e.g. \`admin\`, \`member\`) |
|
||||
| created_at | DateTime64(3, 'UTC') | |
|
||||
|
||||
**team_invitations** table:
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| id | UUID | Primary key |
|
||||
| team_id | UUID | |
|
||||
| team_display_name | String | Snapshot of team name at invite time |
|
||||
| recipient_email | String | |
|
||||
| expires_at_millis | Int64 | Unix milliseconds — compare with \`toUnixTimestamp64Milli(now())\` |
|
||||
| created_at | DateTime64(3, 'UTC') | |
|
||||
|
||||
**email_outboxes** table (transactional + programmatic email delivery log):
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| id | UUID | Primary key |
|
||||
| status | LowCardinality(String) | Raw delivery status |
|
||||
| simple_status | LowCardinality(String) | Collapsed status for reporting |
|
||||
| created_with | LowCardinality(String) | How the email was created |
|
||||
| email_draft_id | Nullable(String) | |
|
||||
| email_programmatic_call_template_id | Nullable(String) | |
|
||||
| theme_id | Nullable(String) | |
|
||||
| is_high_priority | UInt8 (0/1) | |
|
||||
| is_transactional | Nullable(UInt8) | |
|
||||
| subject | Nullable(String) | |
|
||||
| notification_category_id | Nullable(String) | |
|
||||
| started_rendering_at | Nullable(DateTime64(3, 'UTC')) | |
|
||||
| rendered_at | Nullable(DateTime64(3, 'UTC')) | |
|
||||
| render_error | Nullable(String) | Non-null implies render failure |
|
||||
| scheduled_at | DateTime64(3, 'UTC') | |
|
||||
| created_at | DateTime64(3, 'UTC') | |
|
||||
| updated_at | DateTime64(3, 'UTC') | |
|
||||
| started_sending_at | Nullable(DateTime64(3, 'UTC')) | |
|
||||
| server_error | Nullable(String) | Non-null implies send failure |
|
||||
| delivered_at | Nullable(DateTime64(3, 'UTC')) | |
|
||||
| opened_at | Nullable(DateTime64(3, 'UTC')) | |
|
||||
| clicked_at | Nullable(DateTime64(3, 'UTC')) | |
|
||||
| unsubscribed_at | Nullable(DateTime64(3, 'UTC')) | |
|
||||
| marked_as_spam_at | Nullable(DateTime64(3, 'UTC')) | |
|
||||
| bounced_at | Nullable(DateTime64(3, 'UTC')) | |
|
||||
| delivery_delayed_at | Nullable(DateTime64(3, 'UTC')) | |
|
||||
| can_have_delivery_info | Nullable(UInt8) | |
|
||||
| skipped_reason | LowCardinality(Nullable(String)) | Non-null implies send was skipped |
|
||||
| skipped_details | Nullable(String) | |
|
||||
| send_retries | Int32 | |
|
||||
| is_paused | UInt8 (0/1) | |
|
||||
|
||||
Key insights: Most \`*_at\` columns are nullable — use \`IS NOT NULL\` / \`IS NULL\` rather than assuming a value exists. \`delivered_at\`, \`opened_at\`, and \`clicked_at\` are classic funnel steps.
|
||||
|
||||
**project_permissions** table (project-level permissions granted to a user, not scoped to a team):
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| user_id | UUID | |
|
||||
| id | String | Permission identifier |
|
||||
| created_at | DateTime64(3, 'UTC') | |
|
||||
|
||||
**notification_preferences** table (per-user opt-in/out for notification categories):
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| user_id | UUID | |
|
||||
| notification_category_id | String | |
|
||||
| enabled | UInt8 (0/1) | |
|
||||
|
||||
Key insights: No timestamp column — do NOT attempt \`ORDER BY created_at\` or date filtering on this table.
|
||||
|
||||
**refresh_tokens** table (active/past refresh tokens — proxy for sessions):
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| id | UUID | Primary key |
|
||||
| user_id | UUID | |
|
||||
| created_at | DateTime64(3, 'UTC') | Token issue time |
|
||||
| last_used_at | DateTime64(3, 'UTC') | Last time this token was exchanged |
|
||||
| is_impersonation | UInt8 (0/1) | Dashboard/admin impersonation session |
|
||||
| expires_at | Nullable(DateTime64(3, 'UTC')) | Null ⇒ non-expiring |
|
||||
|
||||
**connected_accounts** table (OAuth / SSO provider account links):
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| user_id | UUID | |
|
||||
| provider | String | e.g. \`google\`, \`github\` |
|
||||
| provider_account_id | String | External account identifier |
|
||||
| created_at | DateTime64(3, 'UTC') | Link time |
|
||||
Use \`SHOW TABLES\` to list available tables and \`DESCRIBE TABLE <table_name>\` to see columns, types, and descriptions.
|
||||
Column comments contain important constraints, valid values, and usage notes — always run DESCRIBE TABLE on relevant tables before writing queries.
|
||||
|
||||
### CRITICAL SQL RULES
|
||||
|
||||
@ -1302,11 +1061,11 @@ GROUP BY date, event_type ORDER BY date DESC, event_count DESC LIMIT 100
|
||||
|
||||
### INTERACTION STYLE
|
||||
|
||||
- Generate accurate one-shot queries using the schema above. Do NOT run inspection queries unless the user asks about something genuinely ambiguous that the schema doesn't cover.
|
||||
- Run \`DESCRIBE TABLE\` on the relevant tables first, then generate accurate one-shot queries from what it returns. Re-run it whenever you're unsure about a column, type, or valid value.
|
||||
- Keep chat messages short — the user sees the grid directly.
|
||||
- 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.
|
||||
- If the user asks about event types or data that don't exist (per \`SHOW TABLES\`/\`DESCRIBE TABLE\`), explain what IS available and generate the closest useful query instead.
|
||||
`,
|
||||
|
||||
"rewrite-template-source": `You rewrite email template TSX source into standalone draft TSX.
|
||||
|
||||
@ -18,7 +18,7 @@ export function createSqlQueryTool(targetProjectId?: string | null) {
|
||||
const MAX_ROWS_FOR_AI = 50;
|
||||
|
||||
return tool({
|
||||
description: `Set and validate a ClickHouse SQL query for the analytics data grid. The grid runs the full query independently — you only receive a preview of the first ${MAX_ROWS_FOR_AI} rows to confirm correctness. Only SELECT queries are allowed. Project filtering is automatic. Always include a LIMIT clause.`,
|
||||
description: `Set and validate a ClickHouse SQL query for the analytics data grid. The grid runs the full query independently — you only receive a preview of the first ${MAX_ROWS_FOR_AI} rows to confirm correctness. Only SELECT queries are allowed. Project filtering is automatic. Always include a LIMIT clause. Use SHOW TABLES to discover available tables and DESCRIBE TABLE <table_name> to see columns with types and descriptions.`,
|
||||
inputSchema: z.object({
|
||||
query: z
|
||||
.string()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user