Files
supabase/apps/studio/lib/ai/assistant-context.ts
Charis 21511042a3 feat(studio): assistant logs context and reports guard (#48514)
## 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 — final PR (9/9) of the SQL editor logs-source stack.

**Base branch:** `charislam/sql-editor-inline-ai-clickhouse-dialect` (PR
8). Nothing here is user-visible: entry points stay behind
`sqlEditorLogsSource` + `otelLegacyLogs`, and flag rollout happens after
the whole stack merges.

## What is the current behavior?

- The Assistant has no idea a SQL editor snippet targets the logs
backend. Ask it about a logs snippet and it answers in Postgres, because
the attached query is fenced as ` ```sql ` and nothing tells the model
otherwise.
- Because the `sql` fence is what `MessageMarkdown` treats as runnable
Postgres, an attached ClickHouse query is rendered with a
Run-against-Postgres affordance and branded with `untrustedSql`.
- "Debug with Assistant" on a failed logs query produces a dialect-less
prompt, so both the in-app assistant and the copyable version get
debugged as Postgres.
- A report referencing a `log_sql` snippet runs its ClickHouse SQL
against the user's Postgres database and surfaces the resulting error.

## What is the new behavior?

**Assistant panel.** The "Current Query" chip records which backend the
attached query targets. That reaches the model two ways: each attachment
is fenced with its own dialect (` ```clickhouse ` vs ` ```sql `), and a
`containsLogsSnippets` flag rides on the user message as AI SDK
`metadata`. The server reads the flag off the conversation and prepends
the ClickHouse dialect rules plus the logs schema reference as a
non-cached context message.

Two design points worth calling out in review:

- The flag lives on the **message**, not the request body, so Retry and
the tool-approval continuation reproduce the context a message was
originally asked in — neither of those passes a per-call body.
- It's derived from **what's actually attached**, so detaching the chip
drops the claim rather than leaving the two able to disagree.

The `clickhouse` fence also keeps a logs query out of
`MessageMarkdown`'s `sql` branch, so it's no longer offered as runnable
Postgres or branded with `untrustedSql` — a boundary this stack's
distinct brands exist to prevent crossing.

**Debug flow.** `buildDebugChatArgs` attaches its query with a source
for the same reason, and names the dialect in the prompt text so the
copyable version stands on its own outside the app.

**Reports.** A report only stores a snippet id, so whether it queries
the logs backend is only knowable once the content loads. `ReportBlock`
guards on the fetched type and renders a `LogsSnippetReportBlock`
placeholder instead of executing. Double-guarded: no `sql` for a logs
snippet (so it's out of the query key and `queryFn` short-circuits even
on an explicit `refetch`) and `enabled` excludes it.

**Incidental cleanups.** `buildAssistantContextMessages` extracted out
of `generate-assistant-response`; a schema-access sentinel that was
duplicated as a string literal across two files (and compared against)
replaced with one exported constant; `SqlSnippet` deduplicated to a
single declaration; `resolveSnippetSource` / `isLogsSource` shared
instead of re-implemented per surface.

**Tests.** 4 new/extended suites. Notable cases pinned: a message with
no metadata must validate (`safeValidateUIMessages` applies
`metadataSchema` to *every* message, so a required schema would 400
every existing conversation); only *user* messages count, so a model
reply can't talk the server into a different dialect; a mixed-attachment
message is flagged without overclaiming a single source; and
`ReportBlock` registers no pg-meta mock for the logs cases, so an
unhandled request failing the test *is* the assertion that logs SQL
never reaches Postgres.

Verified: `pnpm typecheck`, `lint:ratchet` (no regression), Prettier,
and the full Studio suite (459 files / 4969 tests).

## Additional context

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

## Summary by CodeRabbit

- **New Features**
- Added support for recognizing log snippets in reports, with clear
guidance to open them in the SQL editor or remove them.
- AI Assistant now understands log snippets and provides
ClickHouse-specific context, formatting, and troubleshooting guidance.
- Snippets retain their source information when shared with the AI
Assistant.

- **Bug Fixes**
- Prevented unsupported log snippets from being executed as regular
database queries.
  - Improved source detection when opening snippets directly from links.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-04 09:02:40 -04:00

80 lines
3.6 KiB
TypeScript

import {
buildClickhouseLogsSchemaSection,
CLICKHOUSE_LOGS_COMPLETION_INSTRUCTIONS,
} from '@/lib/ai/clickhouse-logs'
/**
* Stands in for the schema list when the org hasn't opted into sharing schemas.
* Doubles as a sentinel — "no project context worth sending" is decided by
* comparing against this exact sentence — so the producer and the comparison have
* to agree on it, hence one exported constant instead of copies per call site.
*/
export const NO_SCHEMA_ACCESS_MESSAGE = "You don't have access to any schemas."
/**
* A request-scoped context message, sent as an assistant turn ahead of the
* conversation. Deliberately NOT part of the system prompt: the system prompt is
* static so Bedrock can cache it, and anything derived from the current project,
* chat, or open editor tab would break that cache.
*/
export type AssistantContextMessage = { role: 'assistant'; content: string }
/**
* Tells the model that the snippet in the SQL editor targets the logs backend, so
* SQL it writes for that snippet comes back as ClickHouse for the `logs` table
* rather than Postgres. Carries the same dialect rules and table reference the
* inline editor completions use, so there's one description of the logs schema.
*/
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.",
CLICKHOUSE_LOGS_COMPLETION_INSTRUCTIONS.trim(),
buildClickhouseLogsSchemaSection().trim(),
].join('\n\n')
}
/**
* Assemble the context messages that precede the conversation: what project and
* chat this is, whether it's a support chat, and which backend the open SQL editor
* snippet targets. Pure and per-request — see {@link AssistantContextMessage} for
* why none of this belongs in the system prompt.
*/
export function buildAssistantContextMessages({
projectRef,
chatName,
schemasString,
supportMode,
includesLogsSnippets,
}: {
projectRef?: string
chatName?: string
schemasString: string
supportMode?: boolean
/** Whether any user message in the conversation attached a logs (ClickHouse) query. */
includesLogsSnippets?: boolean
}): AssistantContextMessage[] {
const messages: AssistantContextMessage[] = []
const hasProjectContext = !!projectRef || !!chatName || schemasString !== NO_SCHEMA_ACCESS_MESSAGE
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'}.`,
})
}
if (supportMode) {
messages.push({
role: 'assistant',
content:
'This is an active support chat. Help the user while they wait for a human agent. Keep guidance practical and concise. If the user asks for a human, or if the issue cannot be safely resolved, call escalate_to_human with a short reason. Only call resolve_support_conversation after the user explicitly confirms the issue is resolved; otherwise keep helping.',
})
}
if (includesLogsSnippets) {
messages.push({ role: 'assistant', content: buildLogsSnippetContext() })
}
return messages
}