Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion data/ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,17 @@ search:
references: Additional docs
loading_status_message: Loading Copilot response...
done_loading_status_message: Done loading Copilot response
unable_to_answer: Sorry, I'm unable to answer that question. Check that you selected the correct GitHub version or try a different query.
copy_answer: Copy answer
copied_announcement: Copied!
thumbs_up: This answer was helpful
thumbs_down: This answer was not helpful
thumbs_announcement: Thank you for your feedback!
back_to_search: Back to search
responses:
unable_to_answer: Sorry, I'm unable to answer that question. Check that you selected the correct GitHub version or try a different question.
query_too_large: Sorry, your question is too long. Please try shortening it and asking again.
asked_too_many_times: Sorry, you've asked too many questions in a short time period. Please wait a few minutes and try again.
invalid_query: Sorry, I'm unable to answer that question. Please try asking a different question.
failure:
general_title: There was an error loading search results.
ai_title: There was an error loading Copilot.
Expand Down
6 changes: 5 additions & 1 deletion src/fixtures/fixtures/data/ui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,17 @@ search:
references: Additional docs
loading_status_message: Loading Copilot response...
done_loading_status_message: Done loading Copilot response
unable_to_answer: Sorry, I'm unable to answer that question. Check that you selected the correct GitHub version or try a different query.
copy_answer: Copy answer
copied_announcement: Copied!
thumbs_up: This answer was helpful
thumbs_down: This answer was not helpful
thumbs_announcement: Thank you for your feedback!
back_to_search: Back to search
responses:
unable_to_answer: Sorry, I'm unable to answer that question. Check that you selected the correct GitHub version or try a different question.
query_too_large: Sorry, your question is too long. Please try shortening it and asking again.
asked_too_many_times: Sorry, you've asked too many questions in a short time period. Please wait a few minutes and try again.
invalid_query: Sorry, I'm unable to answer that question. Please try asking a different question.
failure:
general_title: There was an error loading search results.
ai_title: There was an error loading Copilot.
Expand Down
86 changes: 64 additions & 22 deletions src/search/components/input/AskAIResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type AIQueryResultsProps = {
askAIEventGroupId: React.MutableRefObject<string>
aiCouldNotAnswer: boolean
setAICouldNotAnswer: (aiCouldNotAnswer: boolean) => void
listElementsRef: React.RefObject<Array<HTMLLIElement | null>>
}

type AISearchResultEventParams = {
Expand All @@ -56,6 +57,7 @@ export function AskAIResults({
askAIEventGroupId,
aiCouldNotAnswer,
setAICouldNotAnswer,
listElementsRef,
}: AIQueryResultsProps) {
const router = useRouter()
const { t } = useTranslation('search')
Expand All @@ -78,27 +80,30 @@ export function AskAIResults({

const [conversationId, setConversationId] = useState<string>('')

const handleAICannotAnswer = (passedConversationId?: string) => {
const handleAICannotAnswer = (
passedConversationId?: string,
statusCode = 400,
uiMessage = t('search.ai.responses.unable_to_answer'),
) => {
setInitialLoading(false)
setResponseLoading(false)
setAICouldNotAnswer(true)
const cannedResponse = t('search.ai.unable_to_answer')
sendAISearchResultEvent({
sources: [],
message: cannedResponse,
message: uiMessage,
eventGroupId: askAIEventGroupId.current,
couldNotAnswer: true,
status: 400,
status: statusCode,
connectedEventId: passedConversationId || conversationId,
})
setMessage(cannedResponse)
setAnnouncement(cannedResponse)
setMessage(uiMessage)
setAnnouncement(uiMessage)
setReferences([])
setItem(
query,
{
query,
message: cannedResponse,
message: uiMessage,
sources: [],
aiCouldNotAnswer: true,
connectedEventId: passedConversationId || conversationId,
Expand Down Expand Up @@ -156,17 +161,44 @@ export function AskAIResults({
try {
const response = await executeAISearch(router, version, query, debug)
if (!response.ok) {
console.error(
`Failed to fetch search results.\nStatus ${response.status}\n${response.statusText}`,
)
sendAISearchResultEvent({
sources: [],
message: '',
eventGroupId: askAIEventGroupId.current,
couldNotAnswer: false,
status: response.status,
})
return setAISearchError()
// If there is JSON and the `upstreamStatus` key, the error is from the upstream sever (CSE)
let responseJson
try {
responseJson = await response.json()
} catch (error) {
console.error('Failed to parse JSON:', error)
}
const upstreamStatus = responseJson?.upstreamStatus
// If there is no upstream status, the error is either on our end or a 500 from CSE, so we can show the error
if (!upstreamStatus) {
console.error(
`Failed to fetch search results.\nStatus ${response.status}\n${response.statusText}`,
)
sendAISearchResultEvent({
sources: [],
message: '',
eventGroupId: askAIEventGroupId.current,
couldNotAnswer: false,
status: response.status,
})
return setAISearchError()
// Query invalid - either sensitive question or spam
} else if (upstreamStatus === 400 || upstreamStatus === 422) {
return handleAICannotAnswer('', upstreamStatus, t('search.ai.responses.invalid_query'))
// Query too large
} else if (upstreamStatus === 413) {
return handleAICannotAnswer(
'',
upstreamStatus,
t('search.ai.responses.query_too_large'),
)
} else if (upstreamStatus === 429) {
return handleAICannotAnswer(
'',
upstreamStatus,
t('search.ai.responses.asked_too_many_times'),
)
}
} else {
setAISearchError(false)
}
Expand Down Expand Up @@ -209,7 +241,7 @@ export function AskAIResults({
return
}
} catch (e) {
console.error(
console.warn(
'Failed to parse JSON:',
e,
'Line:',
Expand All @@ -226,7 +258,7 @@ export function AskAIResults({
setConversationId(parsedLine.conversation_id)
} else if (parsedLine.chunkType === 'NO_CONTENT_SIGNAL') {
// Serve canned response. A question that cannot be answered was asked
handleAICannotAnswer(conversationIdBuffer)
handleAICannotAnswer(conversationIdBuffer, 200)
} else if (parsedLine.chunkType === 'SOURCES') {
if (!isCancelled) {
sourcesBuffer = sourcesBuffer.concat(parsedLine.sources)
Expand All @@ -240,7 +272,11 @@ export function AskAIResults({
}
} else if (parsedLine.chunkType === 'INPUT_CONTENT_FILTER') {
// Serve canned response. A spam question was asked
handleAICannotAnswer(conversationIdBuffer)
handleAICannotAnswer(
conversationIdBuffer,
200,
t('search.ai.responses.invalid_query'),
)
}
if (!isCancelled) {
setAnnouncement('Copilot Response Loading...')
Expand Down Expand Up @@ -396,6 +432,7 @@ export function AskAIResults({
if (index >= MAX_REFERENCES_TO_SHOW) {
return null
}
const refIndex = index + referencesIndexOffset
return (
<ActionList.Item
sx={{
Expand All @@ -408,7 +445,12 @@ export function AskAIResults({
onSelect={() => {
referenceOnSelect(source.url)
}}
active={index + referencesIndexOffset === selectedIndex}
active={refIndex === selectedIndex}
ref={(element) => {
if (listElementsRef.current) {
listElementsRef.current[refIndex] = element
}
}}
>
<ActionList.LeadingVisual aria-hidden="true">
<FileIcon />
Expand Down
24 changes: 19 additions & 5 deletions src/search/components/input/SearchOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,15 +417,16 @@ export function SearchOverlay({

// Handle keyboard navigation of suggestions
const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
let optionsLength = listElementsRef.current?.length ?? 0
if (event.key === 'ArrowDown') {
event.preventDefault()
if (combinedOptions.length > 0) {
if (optionsLength > 0) {
let newIndex = 0
// If no item is selected, select the first item
if (selectedIndex === -1) {
newIndex = 0
} else {
newIndex = (selectedIndex + 1) % combinedOptions.length
newIndex = (selectedIndex + 1) % optionsLength
// If we go "out of bounds" (i.e. the index is less than the selected index), unselect the item
if (newIndex < selectedIndex) {
newIndex = -1
Expand All @@ -439,17 +440,23 @@ export function SearchOverlay({
newIndex += 1
}
setSelectedIndex(newIndex)
if (newIndex !== -1 && listElementsRef.current[newIndex]) {
listElementsRef.current[newIndex]?.scrollIntoView({
behavior: 'smooth',
block: 'center',
})
}
}
} else if (event.key === 'ArrowUp') {
event.preventDefault()
if (combinedOptions.length > 0) {
if (optionsLength > 0) {
let newIndex = 0
// If no item is selected, select the last item
if (selectedIndex === -1) {
newIndex = combinedOptions.length - 1
newIndex = optionsLength - 1
} else {
// Otherwise, select the previous item
newIndex = (selectedIndex - 1 + combinedOptions.length) % combinedOptions.length
newIndex = (selectedIndex - 1 + optionsLength) % optionsLength
// If we go "out of bounds" (i.e. the index is greater than the selected index), unselect the item
if (newIndex > selectedIndex) {
newIndex = -1
Expand All @@ -464,6 +471,12 @@ export function SearchOverlay({
newIndex -= 1
}
setSelectedIndex(newIndex)
if (newIndex !== -1 && listElementsRef.current[newIndex]) {
listElementsRef.current[newIndex]?.scrollIntoView({
behavior: 'smooth',
block: 'center',
})
}
}
} else if (event.key === 'Enter') {
event.preventDefault()
Expand Down Expand Up @@ -877,6 +890,7 @@ function renderSearchGroups(
askAIEventGroupId={askAIState.askAIEventGroupId}
aiCouldNotAnswer={askAIState.aiCouldNotAnswer}
setAICouldNotAnswer={askAIState.setAICouldNotAnswer}
listElementsRef={listElementsRef}
/>
</ActionList.Group>,
)
Expand Down
24 changes: 9 additions & 15 deletions src/search/lib/ai-search-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import got from 'got'
import { getHmacWithEpoch } from '@/search/lib/helpers/get-cse-copilot-auth'
import { getCSECopilotSource } from '@/search/lib/helpers/cse-copilot-docs-versions'

const memoryCache = new Map<string, Buffer>()

export const aiSearchProxy = async (req: Request, res: Response) => {
const { query, version, language } = req.body

Expand Down Expand Up @@ -43,15 +41,6 @@ export const aiSearchProxy = async (req: Request, res: Response) => {
]
statsd.increment('ai-search.call', 1, diagnosticTags)

// TODO: Caching here may cause an issue if the cache grows too large. Additionally, the cache will be inconsistent across pods
const cacheKey = `${query}:${version}:${language}`
if (memoryCache.has(cacheKey)) {
statsd.increment('ai-search.cache_hit', 1, diagnosticTags)
res.setHeader('Content-Type', 'application/x-ndjson')
res.send(memoryCache.get(cacheKey))
return
}

const startTime = Date.now()
let totalChars = 0

Expand Down Expand Up @@ -84,7 +73,10 @@ export const aiSearchProxy = async (req: Request, res: Response) => {
const errorMessage = `Upstream server responded with status code ${upstreamResponse.statusCode}`
console.error(errorMessage)
statsd.increment('ai-search.stream_response_error', 1, diagnosticTags)
res.status(500).json({ errors: [{ message: errorMessage }] })
res.status(upstreamResponse.statusCode).json({
errors: [{ message: errorMessage }],
upstreamStatus: upstreamResponse.statusCode,
})
stream.destroy()
} else {
// Set response headers
Expand All @@ -101,9 +93,11 @@ export const aiSearchProxy = async (req: Request, res: Response) => {
console.error('Error streaming from cse-copilot:', error)

if (error?.code === 'ERR_NON_2XX_3XX_RESPONSE') {
return res
.status(400)
.json({ errors: [{ message: 'Sorry I am unable to answer this question.' }] })
const upstreamStatus = error?.response?.statusCode || 500
return res.status(upstreamStatus).json({
errors: [{ message: 'Upstream server error' }],
upstreamStatus,
})
}

statsd.increment('ai-search.stream_error', 1, diagnosticTags)
Expand Down
Loading