AIAssistant.utils.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import { isToolUIPart, type UIMessage } from 'ai'
  2. import { toast } from 'sonner'
  3. import { SAFE_FUNCTIONS } from './AiAssistant.constants'
  4. import { authKeys } from '@/data/auth/keys'
  5. import { databaseExtensionsKeys } from '@/data/database-extensions/keys'
  6. import { databaseIndexesKeys } from '@/data/database-indexes/keys'
  7. import { databasePoliciesKeys } from '@/data/database-policies/keys'
  8. import { databaseTriggerKeys } from '@/data/database-triggers/keys'
  9. import { databaseKeys } from '@/data/database/keys'
  10. import { enumeratedTypesKeys } from '@/data/enumerated-types/keys'
  11. import { handleError } from '@/data/fetchers'
  12. import { tableKeys } from '@/data/tables/keys'
  13. import { tryParseJson } from '@/lib/helpers'
  14. import { ResponseError } from '@/types'
  15. export type MutationCategory = 'functions' | 'rls-policies'
  16. // [Joshen] This is just very basic identification, but possible can extend perhaps
  17. export const identifyQueryType = (query: string): MutationCategory | undefined => {
  18. const formattedQuery = query.toLowerCase().replaceAll('\n', ' ')
  19. if (
  20. formattedQuery.includes('create function') ||
  21. formattedQuery.includes('create or replace function')
  22. ) {
  23. return 'functions'
  24. } else if (formattedQuery.includes('create policy') || formattedQuery.includes('alter policy')) {
  25. return 'rls-policies'
  26. }
  27. return undefined
  28. }
  29. // Check for function calls that aren't in the safe list
  30. /** @deprecated [Joshen] Ideally we move away from this as this isn't a scalable way to deduce */
  31. export const containsUnknownFunction = (query: string) => {
  32. const normalizedQuery = query.trim().toLowerCase()
  33. const functionCallRegex = /\w+\s*\(/g
  34. const functionCalls = normalizedQuery.match(functionCallRegex) || []
  35. return functionCalls.some((func) => {
  36. const isReadOnlyFunc = SAFE_FUNCTIONS.some((safeFunc) => func.trim().toLowerCase() === safeFunc)
  37. return !isReadOnlyFunc
  38. })
  39. }
  40. /** @deprecated
  41. * [Joshen] This isn't really a scalable way to reduce this behaviour, we now have support
  42. * for a readonly connection string which we can use this to run queries, and is a much
  43. * clearer way to deduce if the query is read only or not
  44. */
  45. export const isReadOnlySelect = (query: string): boolean => {
  46. const normalizedQuery = query.trim().toLowerCase()
  47. // Check if it starts with SELECT
  48. if (!normalizedQuery.startsWith('select')) return false
  49. // List of keywords that indicate write operations
  50. const writeOperations = ['insert', 'update', 'delete', 'alter', 'drop', 'create', 'replace']
  51. // Words that may appear in column names etc
  52. const allowedPatterns = ['created', 'inserted', 'updated', 'deleted', 'truncate']
  53. // Check for any write operations
  54. const hasWriteOperation = writeOperations.some((op) => {
  55. // Ignore if part of allowed pattern
  56. const isAllowed = allowedPatterns.some(
  57. (allowed) => normalizedQuery.includes(allowed) && allowed.includes(op)
  58. )
  59. return !isAllowed && normalizedQuery.includes(op)
  60. })
  61. if (hasWriteOperation) return false
  62. const hasUnknownFunction = containsUnknownFunction(normalizedQuery)
  63. if (hasUnknownFunction) return false
  64. return true
  65. }
  66. export const hasPendingToolApproval = (messages: Pick<UIMessage, 'role' | 'parts'>[]) => {
  67. return messages.some((message) => {
  68. if (message.role !== 'assistant') return false
  69. return message.parts?.some((part) => isToolUIPart(part) && part.state === 'approval-requested')
  70. })
  71. }
  72. export const resolvePendingToolApprovalsAsDenied = (messages: UIMessage[]): UIMessage[] => {
  73. return messages.map((message) => {
  74. if (message.role !== 'assistant') return message
  75. const parts = message.parts?.map((part) => {
  76. if (!isToolUIPart(part) || part.state !== 'approval-requested') return part
  77. return {
  78. ...part,
  79. state: 'output-denied',
  80. approval: {
  81. id: part.approval.id,
  82. approved: false,
  83. reason: 'Skipped because the user sent a follow-up message.',
  84. },
  85. } as UIMessage['parts'][number]
  86. })
  87. return { ...message, parts } as UIMessage
  88. })
  89. }
  90. const getContextKey = (pathname: string) => {
  91. const [, , , ...rest] = pathname.split('/')
  92. const key = rest.join('/')
  93. return key
  94. }
  95. export const getContextualInvalidationKeys = ({
  96. ref,
  97. pathname,
  98. schema = 'public',
  99. }: {
  100. ref: string
  101. pathname: string
  102. schema?: string
  103. }) => {
  104. const key = getContextKey(pathname)
  105. return (
  106. (
  107. {
  108. 'auth/users': [authKeys.usersInfinite(ref)],
  109. 'auth/policies': [databasePoliciesKeys.list(ref)],
  110. 'database/functions': [databaseKeys.databaseFunctions(ref)],
  111. 'database/tables': [tableKeys.list(ref, schema, true), tableKeys.list(ref, schema, false)],
  112. 'database/triggers': [databaseTriggerKeys.list(ref)],
  113. 'database/types': [enumeratedTypesKeys.list(ref)],
  114. 'database/extensions': [databaseExtensionsKeys.list(ref)],
  115. 'database/indexes': [databaseIndexesKeys.list(ref, schema)],
  116. } as const
  117. )[key] ?? []
  118. )
  119. }
  120. export const onErrorChat = (error: Error) => {
  121. const parsedError = error ? tryParseJson(error.message) : undefined
  122. try {
  123. handleError(parsedError?.error || parsedError || error)
  124. } catch (e: any) {
  125. if (e instanceof ResponseError) {
  126. toast.error(e.message)
  127. } else if (e instanceof Error) {
  128. toast.error(e.message)
  129. } else if (typeof e === 'string') {
  130. toast.error(e)
  131. } else {
  132. toast.error('An unknown error occurred')
  133. }
  134. }
  135. }