generate-assistant-response.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. import * as ai from 'ai'
  2. import {
  3. convertToModelMessages,
  4. isToolUIPart,
  5. stepCountIs,
  6. type LanguageModel,
  7. type ModelMessage,
  8. type SystemModelMessage,
  9. type ToolSet,
  10. type UIMessage,
  11. } from 'ai'
  12. import { startSpan, traced, withCurrent, wrapAISDK, type Span } from 'braintrust'
  13. import { source } from 'common-tags'
  14. import type { AssistantEvalInput } from '@/evals/scorer'
  15. import type { AiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
  16. import { IS_TRACING_ENABLED } from '@/lib/ai/braintrust-logger'
  17. import { CHAT_PROMPT, GENERAL_PROMPT, LIMITATIONS_PROMPT, SECURITY_PROMPT } from '@/lib/ai/prompts'
  18. import { sanitizeMessagePart } from '@/lib/ai/tools/tool-sanitizer'
  19. const { streamText: tracedStreamText } = wrapAISDK(ai)
  20. export async function generateAssistantResponse({
  21. messages: rawMessages,
  22. model,
  23. tools,
  24. aiOptInLevel = 'schema',
  25. getSchemas,
  26. projectRef,
  27. chatId,
  28. chatName,
  29. allowTracing,
  30. userId,
  31. orgId,
  32. planId,
  33. systemProviderOptions,
  34. providerOptions,
  35. requestedModel,
  36. abortSignal,
  37. onSpanCreated,
  38. }: {
  39. messages: UIMessage[]
  40. model: LanguageModel
  41. tools: ToolSet
  42. aiOptInLevel?: AiOptInLevel
  43. getSchemas?: () => Promise<string>
  44. projectRef?: string
  45. chatId?: string
  46. chatName?: string
  47. allowTracing?: boolean
  48. userId?: string
  49. orgId?: number
  50. planId?: string
  51. requestedModel?: string
  52. systemProviderOptions?: Record<string, any>
  53. providerOptions?: Record<string, any>
  54. abortSignal?: AbortSignal
  55. onSpanCreated?: (spanId: string) => void
  56. }) {
  57. const shouldTrace = allowTracing ?? IS_TRACING_ENABLED
  58. const run = async (span?: Span) => {
  59. // Only returns last 7 messages
  60. // Filters out tools with invalid states
  61. // Filters out tool outputs based on opt-in level
  62. const messages = (rawMessages || []).slice(-7).map((msg) => {
  63. if (msg && msg.role === 'assistant' && 'results' in msg) {
  64. const cleanedMsg = { ...msg }
  65. delete cleanedMsg.results
  66. return cleanedMsg
  67. }
  68. if (msg && msg.role === 'assistant' && msg.parts) {
  69. const cleanedParts = msg.parts
  70. .filter((part) => {
  71. if (isToolUIPart(part)) {
  72. const invalidStates = [
  73. 'input-streaming',
  74. 'input-available',
  75. 'approval-requested',
  76. 'output-error',
  77. ]
  78. return !invalidStates.includes(part.state)
  79. }
  80. return true
  81. })
  82. .map((part) => {
  83. return sanitizeMessagePart(part, aiOptInLevel)
  84. })
  85. return { ...msg, parts: cleanedParts }
  86. }
  87. return msg
  88. })
  89. const schemasString =
  90. aiOptInLevel !== 'disabled' && getSchemas
  91. ? shouldTrace
  92. ? await traced(async () => getSchemas(), { name: 'getSchemas', type: 'function' })
  93. : await getSchemas()
  94. : "You don't have access to any schemas."
  95. // Important: do not use dynamic content in the system prompt or Bedrock will not cache it
  96. const system = source`
  97. ${GENERAL_PROMPT}
  98. ${CHAT_PROMPT}
  99. ${SECURITY_PROMPT}
  100. ${LIMITATIONS_PROMPT}
  101. ## Available Knowledge
  102. Before writing SQL or answering questions about the following topics, call \`load_knowledge\` to load detailed knowledge:
  103. - \`pg_best_practices\` — PostgreSQL best practices. Always load before writing any SQL, even simple queries.
  104. - \`rls\` — Row Level Security policies
  105. - \`edge_functions\` — Briven Edge Functions
  106. - \`realtime\` — Briven Realtime
  107. `
  108. const hasProjectContext =
  109. projectRef || chatName || schemasString !== "You don't have access to any schemas."
  110. const assistantContent = hasProjectContext
  111. ? `The user's current project is ${projectRef || 'unknown'}. Their available schemas are: ${schemasString}. The current chat name is: ${chatName || 'unnamed'}.`
  112. : undefined
  113. const systemMessage: SystemModelMessage = {
  114. role: 'system',
  115. content: system,
  116. ...(systemProviderOptions && { providerOptions: systemProviderOptions }),
  117. }
  118. const coreMessages: ModelMessage[] = [
  119. ...(assistantContent
  120. ? [
  121. {
  122. role: 'assistant' as const,
  123. content: assistantContent,
  124. },
  125. ]
  126. : []),
  127. ...(await convertToModelMessages(messages)),
  128. ]
  129. const streamTextFn = shouldTrace ? tracedStreamText : ai.streamText
  130. return streamTextFn({
  131. model,
  132. system: systemMessage,
  133. stopWhen: stepCountIs(5),
  134. messages: coreMessages,
  135. ...(providerOptions && { providerOptions }),
  136. tools,
  137. ...(abortSignal && { abortSignal }),
  138. ...(span && {
  139. onFinish: ({ steps, finishReason }) => {
  140. const metadata: Record<string, unknown> = {
  141. isFinalStep: finishReason === 'stop',
  142. }
  143. for (const step of steps) {
  144. for (const toolCall of step.toolCalls) {
  145. if (toolCall.toolName === 'rename_chat') {
  146. const { newName } = toolCall.input as { newName: string }
  147. metadata.chatName = newName
  148. }
  149. }
  150. }
  151. span.log({ metadata })
  152. span.end()
  153. },
  154. }),
  155. } satisfies Parameters<typeof ai.streamText>[0])
  156. }
  157. if (shouldTrace) {
  158. // startSpan instead of traced() so we control when the span closes via onFinish.
  159. // Scorers read from child spans (LLM + tool) in the trace rather than a root span output field.
  160. const span = startSpan({ name: 'generateAssistantResponse', type: 'function' })
  161. onSpanCreated?.(span.id)
  162. const lastUserMessage = rawMessages.findLast((m) => m.role === 'user')
  163. const lastUserText = lastUserMessage?.parts
  164. ?.filter((p): p is { type: 'text'; text: string } => p.type === 'text')
  165. .map((p) => p.text)
  166. .join('\n')
  167. span.log({
  168. input: { prompt: lastUserText ?? '' } satisfies AssistantEvalInput,
  169. metadata: {
  170. projectRef,
  171. chatId,
  172. chatName,
  173. aiOptInLevel,
  174. userId,
  175. orgId,
  176. planId,
  177. requestedModel,
  178. gitBranch: process.env.VERCEL_GIT_COMMIT_REF,
  179. environment: process.env.NEXT_PUBLIC_ENVIRONMENT,
  180. },
  181. })
  182. return withCurrent(span, () => run(span))
  183. }
  184. return run()
  185. }