Files
supabase/apps/docs/lib/openAi.ts
Charis df4b1867b8 feat (content api): add global search query (#35290)
Add a top-level field to search docs globally. Right now this only
returns Markdown guides (not references, GitHub discussions, or partner
pages.

The full GraphQL schema at this point:

```
schema {
  query: RootQueryType
}

type RootQueryType {
  """Get the GraphQL schema for this endpoint"""
  schema: String!

  """Search the Supabase docs for content matching a query string"""
  searchDocs(query: String!, limit: Int): SearchResultCollection
}

"""A collection of search results containing content from Supabase docs"""
type SearchResultCollection {
  """A list of edges containing nodes in this collection"""
  edges: [SearchResultEdge!]!

  """The nodes in this collection, directly accessible"""
  nodes: [SearchResult!]!

  """The total count of items available in this collection"""
  totalCount: Int!
}

"""An edge in a collection of SearchResults"""
type SearchResultEdge {
  """The SearchResult at the end of the edge"""
  node: SearchResult!
}

"""Document that matches a search query"""
interface SearchResult {
  """The title of the matching result"""
  title: String

  """The URL of the matching result"""
  href: String

  """The full content of the matching result"""
  content: String
}
```

Towards DOCS-214
2025-05-05 14:32:10 -04:00

68 lines
1.8 KiB
TypeScript

import OpenAI from 'openai'
import 'server-only'
import {
convertUnknownToApiError,
InvalidRequestError,
type ApiError,
type ApiErrorGeneric,
} from '~/app/api/utils'
import { Result } from '~/features/helpers.fn'
type Embedding = Array<number>
interface ModerationFlaggedDetails {
flagged: boolean
categories: OpenAI.Moderations.Moderation.Categories
}
export interface OpenAIClientInterface {
createContentEmbedding(text: string): Promise<Result<Embedding, ApiErrorGeneric>>
}
let openAIClient: OpenAIClientInterface | null
class OpenAIClient implements OpenAIClientInterface {
static CONTENT_EMBEDDING_MODEL = 'text-embedding-ada-002'
constructor(private client: OpenAI) {}
async createContentEmbedding(text: string): Promise<Result<Embedding, ApiErrorGeneric>> {
return await Result.tryCatchFlat(
this.createContentEmbeddingImpl.bind(this),
convertUnknownToApiError,
text
)
}
private async createContentEmbeddingImpl(
text: string
): Promise<Result<Embedding, ApiError<ModerationFlaggedDetails>>> {
const query = text.trim()
const moderationResponse = await this.client.moderations.create({ input: query })
const [result] = moderationResponse.results
if (result.flagged) {
return Result.error(
new InvalidRequestError('Content flagged as inappropriate', undefined, {
flagged: true,
categories: result.categories,
})
)
}
const embeddingsResponse = await this.client.embeddings.create({
model: OpenAIClient.CONTENT_EMBEDDING_MODEL,
input: query,
})
const [{ embedding: queryEmbedding }] = embeddingsResponse.data
return Result.ok(queryEmbedding)
}
}
export function openAI(): OpenAIClientInterface {
if (!openAIClient) {
openAIClient = new OpenAIClient(new OpenAI())
}
return openAIClient
}