Skip to content

Commit 046bf35

Browse files
committed
feat: implement caching for VFB term info and run query with retry logic for transient errors
1 parent 6e13d7a commit 046bf35

1 file changed

Lines changed: 138 additions & 8 deletions

File tree

app/api/chat/route.js

Lines changed: 138 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,96 @@ const MCP_TOOL_ROUTING = {
661661
biorxiv_get_categories: { server: 'biorxiv', mcpName: 'get_categories' }
662662
}
663663

664+
const VFB_CACHED_TERM_INFO_URL = 'https://v3-cached.virtualflybrain.org/get_term_info'
665+
const VFB_CACHED_RUN_QUERY_URL = 'https://v3-cached.virtualflybrain.org/run_query'
666+
const VFB_CACHED_TERM_INFO_TIMEOUT_MS = 12000
667+
668+
function isRetryableMcpError(error) {
669+
const message = `${error?.name || ''} ${error?.message || ''}`.toLowerCase()
670+
return (
671+
message.includes('timeout') ||
672+
message.includes('timed out') ||
673+
message.includes('abort') ||
674+
message.includes('network') ||
675+
message.includes('fetch failed') ||
676+
message.includes('econnreset') ||
677+
message.includes('econnrefused') ||
678+
message.includes('enotfound') ||
679+
message.includes('etimedout') ||
680+
message.includes('eai_again') ||
681+
message.includes('connectivity')
682+
)
683+
}
684+
685+
async function fetchCachedVfbTermInfo(id) {
686+
const safeId = String(id || '').trim()
687+
if (!safeId) throw new Error('Missing id for cached VFB get_term_info fallback.')
688+
689+
const controller = new AbortController()
690+
const timeoutId = setTimeout(() => controller.abort(), VFB_CACHED_TERM_INFO_TIMEOUT_MS)
691+
692+
try {
693+
const cacheUrl = `${VFB_CACHED_TERM_INFO_URL}?id=${encodeURIComponent(safeId)}`
694+
const response = await fetch(cacheUrl, {
695+
method: 'GET',
696+
headers: { Accept: 'application/json' },
697+
signal: controller.signal
698+
})
699+
700+
if (!response.ok) {
701+
const responseText = await response.text()
702+
throw new Error(`Cached VFB get_term_info failed: HTTP ${response.status} ${responseText.slice(0, 200)}`.trim())
703+
}
704+
705+
const responseText = await response.text()
706+
try {
707+
JSON.parse(responseText)
708+
} catch {
709+
throw new Error('Cached VFB get_term_info returned non-JSON payload.')
710+
}
711+
712+
return responseText
713+
} finally {
714+
clearTimeout(timeoutId)
715+
}
716+
}
717+
718+
async function fetchCachedVfbRunQuery(id, queryType) {
719+
const safeId = String(id || '').trim()
720+
const safeQueryType = String(queryType || '').trim()
721+
if (!safeId || !safeQueryType) {
722+
throw new Error('Missing id or query_type for cached VFB run_query fallback.')
723+
}
724+
725+
const controller = new AbortController()
726+
const timeoutId = setTimeout(() => controller.abort(), VFB_CACHED_TERM_INFO_TIMEOUT_MS)
727+
728+
try {
729+
const cacheUrl = `${VFB_CACHED_RUN_QUERY_URL}?id=${encodeURIComponent(safeId)}&query_type=${encodeURIComponent(safeQueryType)}`
730+
const response = await fetch(cacheUrl, {
731+
method: 'GET',
732+
headers: { Accept: 'application/json' },
733+
signal: controller.signal
734+
})
735+
736+
if (!response.ok) {
737+
const responseText = await response.text()
738+
throw new Error(`Cached VFB run_query failed: HTTP ${response.status} ${responseText.slice(0, 200)}`.trim())
739+
}
740+
741+
const responseText = await response.text()
742+
try {
743+
JSON.parse(responseText)
744+
} catch {
745+
throw new Error('Cached VFB run_query returned non-JSON payload.')
746+
}
747+
748+
return responseText
749+
} finally {
750+
clearTimeout(timeoutId)
751+
}
752+
}
753+
664754
async function executeFunctionTool(name, args) {
665755
if (name === 'search_pubmed') {
666756
return searchPubmed(args.query, args.max_results, args.sort)
@@ -689,15 +779,55 @@ async function executeFunctionTool(name, args) {
689779
if (value !== undefined && value !== null) cleanArgs[key] = value
690780
}
691781

692-
const result = await client.callTool({ name: routing.mcpName, arguments: cleanArgs })
693-
if (result?.content) {
694-
const texts = result.content
695-
.filter(item => item.type === 'text')
696-
.map(item => item.text)
697-
return texts.join('\n') || JSON.stringify(result.content)
698-
}
782+
try {
783+
const result = await client.callTool({ name: routing.mcpName, arguments: cleanArgs })
784+
if (result?.content) {
785+
const texts = result.content
786+
.filter(item => item.type === 'text')
787+
.map(item => item.text)
788+
return texts.join('\n') || JSON.stringify(result.content)
789+
}
790+
791+
return JSON.stringify(result)
792+
} catch (error) {
793+
const shouldUseCachedTermInfoFallback =
794+
name === 'vfb_get_term_info' &&
795+
routing.server === 'vfb' &&
796+
typeof cleanArgs.id === 'string' &&
797+
cleanArgs.id.trim().length > 0 &&
798+
isRetryableMcpError(error)
799+
800+
if (shouldUseCachedTermInfoFallback) {
801+
try {
802+
return await fetchCachedVfbTermInfo(cleanArgs.id)
803+
} catch (fallbackError) {
804+
throw new Error(
805+
`VFB MCP get_term_info failed (${error?.message || 'unknown error'}); cached fallback failed (${fallbackError?.message || 'unknown error'}).`
806+
)
807+
}
808+
}
699809

700-
return JSON.stringify(result)
810+
const shouldUseCachedRunQueryFallback =
811+
name === 'vfb_run_query' &&
812+
routing.server === 'vfb' &&
813+
typeof cleanArgs.id === 'string' &&
814+
cleanArgs.id.trim().length > 0 &&
815+
typeof cleanArgs.query_type === 'string' &&
816+
cleanArgs.query_type.trim().length > 0 &&
817+
isRetryableMcpError(error)
818+
819+
if (shouldUseCachedRunQueryFallback) {
820+
try {
821+
return await fetchCachedVfbRunQuery(cleanArgs.id, cleanArgs.query_type)
822+
} catch (fallbackError) {
823+
throw new Error(
824+
`VFB MCP run_query failed (${error?.message || 'unknown error'}); cached fallback failed (${fallbackError?.message || 'unknown error'}).`
825+
)
826+
}
827+
}
828+
829+
throw error
830+
}
701831
}
702832

703833
throw new Error(`Unknown function tool: ${name}`)

0 commit comments

Comments
 (0)