feat(studio): teach assistant to query ClickHouse logs (#49292)

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Feature and bug fix.

## What is the current behavior?

The assistant can call `query_logs`, but it is not given the ClickHouse
schema and query-writing guidance it needs. It also lacks a current UTC
reference for producing the absolute timestamps required by the tool,
which can lead to valid queries being run against the wrong time range
and reported as returning zero rows.

## What is the new behavior?

- Adds a dedicated `logs` knowledge topic backed by the shared
ClickHouse schema and query guidance.
- Requires the assistant to load that knowledge before using
`query_logs`.
- Includes the current UTC time in project context so relative requests
can be converted to correct absolute tool parameters.
- Covers the new knowledge flow and context with focused tests and
updates the assistant eval expectation.

## How to test

1. Check out this PR and run Studio against a project that has recent
logs. Generate some project activity first, such as an API request, if
needed.
2. Open the AI Assistant and ask: `Show log counts by minute for the
last 15 minutes and summarize any spikes.`
3. Expand the assistant's tool activity and verify it loads the `logs`
knowledge topic before calling `query_logs`.
4. Inspect the `query_logs` input and verify:
- `iso_timestamp_start` and `iso_timestamp_end` are absolute UTC
timestamps ending in `Z`.
   - The timestamps cover approximately the requested 15-minute window.
- The SQL uses ClickHouse syntax, includes a `LIMIT`, and does not put
the time range in the SQL `WHERE` clause.
5. Verify the assistant's summary reflects the rows returned by
`query_logs` instead of reporting zero rows when results are present.

## Additional context

This is the bottom PR in stack #49294. The front-end visualization is
added separately in #49293.

Verified with 59 focused tests across assistant context, Studio/MCP
tools, query display, and logs result parsing.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added AI-assisted project log querying through the `query_logs` tool.
- Added logs knowledge guidance for time ranges, schema discovery, query
limits, and concise result summaries.
- Project context now includes the current UTC timestamp to improve
relative time-range interpretation.
- Improved notebook assistance with safer table verification and
appropriate handling of log queries.

- **Bug Fixes**
- Prevented incorrect SQL timestamp filtering and enabled cross-service
searches without requiring a source filter.
  - Added validation for supported knowledge topics.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Saxon Fletcher authored and GitHub committed 2026-08-21 09:30:54 +10:00
1 parent 70715790b8
commit aa2897f712
8 files changed
+85 -5

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ export const dataset: AssistantEvalCase[] = [
},
expected: {
requiredTools: ['get_advisors', 'query_logs'],
requiredKnowledge: ['logs'],
},
metadata: { category: ['debugging', 'rls_policies'] },
},
@@ -10,6 +10,7 @@ describe('buildAssistantContextMessages', () => {
projectRef: 'abcdefghijklmnopqrst',
chatName: 'Slow queries',
schemasString: SCHEMAS,
now: new Date('2026-08-20T06:53:00.000Z'),
})
expect(messages).toHaveLength(1)
@@ -17,6 +18,8 @@ describe('buildAssistantContextMessages', () => {
expect(messages[0].content).toContain('abcdefghijklmnopqrst')
expect(messages[0].content).toContain(SCHEMAS)
expect(messages[0].content).toContain('Slow queries')
expect(messages[0].content).toContain('2026-08-20T06:53:00.000Z')
expect(messages[0].content).toContain('iso_timestamp_start')
})
it('omits the project message when there is nothing to say', () => {
@@ -51,6 +54,8 @@ describe('buildAssistantContextMessages', () => {
// ...and the table reference, so it doesn't invent BigQuery-style unnests.
expect(logsContext).toContain('log_attributes')
expect(logsContext).toContain("where source = 'edge_logs'")
expect(logsContext).toContain('query_logs')
expect(logsContext).not.toContain('cannot run')
})
it('adds nothing extra for a database-only conversation', () => {
+5 -2
View File
@@ -27,7 +27,7 @@ export type AssistantContextMessage = { role: 'assistant'; content: string }
*/
function buildLogsSnippetContext(): string {
return [
"Some SQL snippets are marked with the dialect 'clickhouse', which means they query the Supabase logs backend, not the Postgres database. Any SQL you write, edit, or debug for that snippet must be ClickHouse SQL against the logs table described below — the database schema and the Postgres tools don't apply to it. You can help a user iterate on their ClickHouse SQL query, but you cannot run it for them (the execute_query tool does not run log queries). Postgres SQL is still the right answer for anything else the user asks about their database, or for non-ClickHouse marked queries.",
"Some SQL snippets are marked with the dialect 'clickhouse', which means they query the Supabase logs backend, not the Postgres database. Any SQL you write, edit, or debug for that snippet must be ClickHouse SQL against the logs table described below — the database schema and the Postgres tools don't apply to it. To run a logs query, load `logs` knowledge then call `query_logs`; do not use `execute_sql`. Postgres SQL is still the right answer for anything else the user asks about their database, or for non-ClickHouse marked queries.",
CLICKHOUSE_LOGS_COMPLETION_INSTRUCTIONS.trim(),
buildClickhouseLogsSchemaSection().trim(),
].join('\n\n')
@@ -45,6 +45,7 @@ export function buildAssistantContextMessages({
schemasString,
supportMode,
includesLogsSnippets,
now = new Date(),
}: {
projectRef?: string
chatName?: string
@@ -52,6 +53,8 @@ export function buildAssistantContextMessages({
supportMode?: boolean
/** Whether any user message in the conversation attached a logs (ClickHouse) query. */
includesLogsSnippets?: boolean
/** Injected so tests can pin the clock. Lives here, not the system prompt, so Bedrock can cache the system prompt. */
now?: Date
}): AssistantContextMessage[] {
const messages: AssistantContextMessage[] = []
@@ -59,7 +62,7 @@ export function buildAssistantContextMessages({
if (hasProjectContext) {
messages.push({
role: 'assistant',
content: `The user's current project is ${projectRef || 'unknown'}. Their available schemas are: ${schemasString}. The current chat name is: ${chatName || 'unnamed'}.`,
content: `The user's current project is ${projectRef || 'unknown'}. Their available schemas are: ${schemasString}. The current chat name is: ${chatName || 'unnamed'}. The current time is ${now.toISOString()} (UTC). Use this clock when converting relative ranges such as "last hour" into iso_timestamp_start and iso_timestamp_end.`,
})
}
+4 -2
View File
@@ -13,6 +13,8 @@ You are writing SQL for Supabase logs, which run on a ClickHouse-backed engine.
- All logs are in a single table named \`logs\`, keyed by a \`source\` column. There are no per-service tables (no \`edge_logs\`, \`postgres_logs\`, and so on) and no \`unnest\` joins.
- Per-source fields live in the \`log_attributes\` Map(String, String), read as \`log_attributes['key']\`. Map values are strings, so wrap numeric ones in \`toInt32OrZero(...)\`.
- Use ClickHouse functions, not Postgres or BigQuery ones. Use \`match(col, 'regex')\` or \`col ILIKE '%text%'\` instead of \`regexp_contains\`, \`count()\` instead of \`count(*)\`, and select the \`timestamp\` column directly instead of \`cast(timestamp as datetime)\`.
- Do not filter on \`timestamp\` in SQL and do not wrap it in \`toDateTime64\`, \`toDateTime\`, or \`parseDateTime*\`. The editor applies the selected time range as a request parameter. A trailing \`Z\` inside those functions is invalid ClickHouse.
- Filter by \`source\` to scope to one service; omit it to query across services.
- Do not quote identifiers with double quotes and do not append a trailing semicolon.
- Do not use \`select *\`, this is disallowed by the backend.
`
@@ -27,7 +29,7 @@ const CLICKHOUSE_LOGS_COLUMN_REFERENCE = `The logs table has these columns:
- timestamp (DateTime64, UTC) formatted like 2026-06-22T09:34:06.215000 (ISO 8601, microsecond precision, no trailing Z)
- event_message (String): the raw log line
- severity_text (String): log level when present
- source (String): the service the log belongs to. Always filter by it, e.g. where source = 'edge_logs'.
- source (String): the service the log belongs to. Filter by it to scope to one service, e.g. where source = 'edge_logs'. Omit it to query across services.
- log_attributes (Map(String, String)): structured per-source fields, read as log_attributes['key']
Sources and their common log_attributes keys:
@@ -39,7 +41,7 @@ Sources and their common log_attributes keys:
- function_logs: event_type, function_id, execution_id, level
- storage_logs, realtime_logs, postgrest_logs, supavisor_logs, pgbouncer_logs: mostly id, timestamp, event_message, with extra fields in log_attributes
The editor applies the user's selected time range as a request parameter, so an explicit timestamp filter is usually unnecessary.`
The editor or query_logs tool applies the user's selected time range as a request parameter, so do not add a timestamp filter in SQL.`
function renderAvailableKeys(availableKeys?: string[]): string {
if (!availableKeys || availableKeys.length === 0) return ''
@@ -96,6 +96,7 @@ export async function generateAssistantResponse({
Before writing SQL or answering questions about the following topics, call \`load_knowledge\` to load detailed knowledge:
- \`pg_best_practices\` — PostgreSQL best practices. Always load before writing any SQL, even simple queries.
- \`logs\` — ClickHouse SQL against the project's logs table. Always load before calling \`query_logs\`.
- \`rls\` — Row Level Security policies for database tables.
- \`storage\` — Supabase Storage buckets, public/private bucket access, and \`storage.objects\` policies. Always load before creating Storage buckets or \`storage.objects\` policies.
- \`edge_functions\` — Supabase Edge Functions
+39 -1
View File
@@ -403,6 +403,44 @@ export const PG_BEST_PRACTICES = `
- Use \`create or replace function\` whenever possible.
`
export const LOGS_PROMPT = `
# Querying Supabase logs
Use \`query_logs\`, never \`execute_sql\`, for project logs. The client renders the SQL and result set as an interactive query cell. After the tool returns, summarize the trend or notable outliers in 1–2 sentences. Do not paste the SQL, list rows, or reformat the result as a markdown table.
${CLICKHOUSE_LOGS_COMPLETION_INSTRUCTIONS.trim()}
${buildClickhouseLogsSchemaSection().trim()}
## query_logs rules
- Always \`LIMIT\` (explorer max 1000). Prefer 100 while iterating.
- Start with a \`-- short title\` comment. The client uses it as the result title.
- Time range is a \`query_logs\` parameter, never a SQL filter. For relative windows ("last hour", "last 15 minutes"), compute \`iso_timestamp_start\` and \`iso_timestamp_end\` from the current UTC time in context — do not invent a clock and do not reuse example timestamps. Format as ISO-8601 UTC with a trailing \`Z\`. If the user did not name a window, omit both params (tool default: last 24 hours, max 24 hours).
- Do not guess \`log_attributes\` keys. A missing key returns an empty string, so a wrong key looks like an empty result. Discover keys from recent rows, or read \`event_message\`.
Discover keys:
\`\`\`sql
select arrayJoin(mapKeys(log_attributes)) as key, count() as n
from logs
where source = 'postgres_logs'
group by key
order by n desc
limit 100
\`\`\`
Use ClickHouse time buckets such as \`toStartOfMinute(timestamp)\`, \`toStartOfHour(timestamp)\`, and \`toStartOfDay(timestamp)\`; do not use Postgres \`date_trunc\`.
Example aggregate (pass the time window as tool parameters):
\`\`\`sql
-- counts by minute
select toStartOfMinute(timestamp) as minute, count() as total
from logs
group by minute
order by minute
limit 100
\`\`\`
`
export const REALTIME_PROMPT = `
# Supabase Realtime Implementation Guide
@@ -740,13 +778,13 @@ export const CHAT_PROMPT = `
- Use markdown code blocks (\`\`\`sql\`\`\`) for illustrative SQL only if requested by the user or when providing non-executable examples.
- Never call \`execute_sql\` or \`deploy_edge_function\` in parallel within the same step. Each requires user approval, so issue one per step and wait for its result before calling the next.
- After execution, summarize outcomes concisely without duplicating results, as the client will present these.
- Use \`query_logs\` for project logs (load \`logs\` knowledge first). The tool runs immediately with no confirmation step. The client renders the SQL and results in an interactive cell — do not paste the SQL, list rows, or reformat the result as a markdown table. Summarize the trend or notable outliers in 1–2 sentences.
## Edge Functions
- Deploy Edge Functions by calling \`deploy_edge_function\` directly with \`name\` and \`code\`; the client handles confirmation and result presentation.
- Provide example Edge Function code in markdown code blocks (\`\`\`edge\`\`\` or \`\`\`typescript\`\`\`) only upon user request or for illustrative purposes.
- Use \`deploy_edge_function\` solely for deployment, not for presenting example code.
## Project Health Checks
- Use \`get_advisors\` to identify project issues; if unavailable, suggest the user use the Supabase dashboard.
- Use \`query_logs\` to access recent project logs by running a read-only SQL query against them.
## Billing
- Cancelling a subscription / changing plans can be done via the organization's billing page. Link directly to https://supabase.com/dashboard/org/_/billing.
- To check organization usage, use the organization's usage page. Link directly to https://supabase.com/dashboard/org/_/usage.
@@ -54,6 +54,34 @@ describe('ai/tools/studio-tools', () => {
expect(toolNames).toContain('rename_chat')
})
it('should include logs in the load_knowledge schema', () => {
const tools = getStudioTools()
const schema = tools.load_knowledge.inputSchema
if ('safeParse' in schema) {
expect(schema.safeParse({ name: 'logs' }).success).toBe(true)
expect(schema.safeParse({ name: 'pg_best_practices' }).success).toBe(true)
expect(schema.safeParse({ name: 'not_a_topic' }).success).toBe(false)
} else {
expect(schema).toBeDefined()
}
})
it('should return ClickHouse logs knowledge for load_knowledge logs', async () => {
const tools = getStudioTools()
if (!tools.load_knowledge.execute) throw new Error('execute is undefined')
const result = await tools.load_knowledge.execute(
{ name: 'logs' },
{ toolCallId: 'test', messages: [], context: {} }
)
expect(result).toContain('query_logs')
expect(result).toContain('iso_timestamp_start')
expect(result).toContain('# Supabase logs SQL (ClickHouse)')
expect(result).toContain('interactive query cell')
})
it('should have execute_sql with correct input schema fields', () => {
const tools = getStudioTools()
const executeSqlTool = tools.execute_sql
+2
View File
@@ -7,6 +7,7 @@ import { executeSql } from '@/data/sql/execute-sql-mutation'
import type { AiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
import {
EDGE_FUNCTION_PROMPT,
LOGS_PROMPT,
PG_BEST_PRACTICES,
REALTIME_PROMPT,
RLS_PROMPT,
@@ -21,6 +22,7 @@ const KNOWLEDGE = {
storage: STORAGE_PROMPT,
edge_functions: EDGE_FUNCTION_PROMPT,
realtime: REALTIME_PROMPT,
logs: LOGS_PROMPT,
} as const
type KnowledgeName = keyof typeof KNOWLEDGE