Skip to content

Commit d516908

Browse files
committed
feat: enhance query handling with new response functions and error enrichment logic
1 parent 009b957 commit d516908

1 file changed

Lines changed: 201 additions & 7 deletions

File tree

app/api/chat/route.js

Lines changed: 201 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,19 @@ function createImmediateErrorResponse(message, requestId, responseId) {
9090
})
9191
}
9292

93+
function createImmediateResultResponse(message, requestId, responseId) {
94+
return buildSseResponse(async (sendEvent) => {
95+
sendEvent('result', {
96+
response: message,
97+
images: [],
98+
graphs: [],
99+
newScene: {},
100+
requestId,
101+
responseId
102+
})
103+
})
104+
}
105+
93106
function getClientIp(request) {
94107
const xForwardedFor = request.headers.get('x-forwarded-for') || ''
95108
return (xForwardedFor.split(',')[0] || '').trim() || request.headers.get('x-real-ip') || 'unknown'
@@ -1423,6 +1436,40 @@ async function fetchCachedVfbRunQuery(id, queryType) {
14231436
}
14241437
}
14251438

1439+
function extractQueryNamesFromTermInfoPayload(rawPayload) {
1440+
let parsed = rawPayload
1441+
if (typeof rawPayload === 'string') {
1442+
try {
1443+
parsed = JSON.parse(rawPayload)
1444+
} catch {
1445+
return []
1446+
}
1447+
}
1448+
1449+
if (!parsed || typeof parsed !== 'object') return []
1450+
1451+
const candidateRecords = []
1452+
if (Array.isArray(parsed.Queries)) {
1453+
candidateRecords.push(parsed)
1454+
}
1455+
1456+
for (const value of Object.values(parsed)) {
1457+
if (value && typeof value === 'object' && Array.isArray(value.Queries)) {
1458+
candidateRecords.push(value)
1459+
}
1460+
}
1461+
1462+
const queryNames = []
1463+
for (const record of candidateRecords) {
1464+
for (const entry of record.Queries || []) {
1465+
const queryName = typeof entry?.query === 'string' ? entry.query.trim() : ''
1466+
if (queryName) queryNames.push(queryName)
1467+
}
1468+
}
1469+
1470+
return Array.from(new Set(queryNames))
1471+
}
1472+
14261473
function createBasicGraph(args = {}) {
14271474
const normalized = normalizeGraphSpec(args)
14281475
if (!normalized) {
@@ -1510,6 +1557,47 @@ async function executeFunctionTool(name, args) {
15101557
}
15111558
}
15121559

1560+
const shouldEnrichRunQueryError =
1561+
name === 'vfb_run_query' &&
1562+
routing.server === 'vfb' &&
1563+
typeof cleanArgs.id === 'string' &&
1564+
cleanArgs.id.trim().length > 0 &&
1565+
typeof cleanArgs.query_type === 'string' &&
1566+
cleanArgs.query_type.trim().length > 0 &&
1567+
/\b(query[_\s-]?type|invalid query|not available for this id|not a valid query|available queries|http\s*400|status code 400|bad request)\b/i.test(error?.message || '')
1568+
1569+
if (shouldEnrichRunQueryError) {
1570+
let termInfoPayload = null
1571+
1572+
try {
1573+
const termInfoResult = await client.callTool({
1574+
name: 'get_term_info',
1575+
arguments: { id: cleanArgs.id }
1576+
})
1577+
const termInfoText = termInfoResult?.content
1578+
?.filter(item => item.type === 'text')
1579+
?.map(item => item.text)
1580+
?.join('\n')
1581+
1582+
if (termInfoText) termInfoPayload = termInfoText
1583+
} catch (termInfoError) {
1584+
if (isRetryableMcpError(termInfoError)) {
1585+
try {
1586+
termInfoPayload = await fetchCachedVfbTermInfo(cleanArgs.id)
1587+
} catch {
1588+
// Keep the original run_query error when enrichment lookup fails.
1589+
}
1590+
}
1591+
}
1592+
1593+
const availableQueryTypes = extractQueryNamesFromTermInfoPayload(termInfoPayload)
1594+
if (availableQueryTypes.length > 0) {
1595+
throw new Error(
1596+
`${error?.message || 'run_query failed'}. Available query_type values for ${cleanArgs.id}: ${availableQueryTypes.join(', ')}.`
1597+
)
1598+
}
1599+
}
1600+
15131601
const shouldUseBiorxivApiFallback = routing.server === 'biorxiv' && BIORXIV_TOOL_NAME_SET.has(name)
15141602
if (shouldUseBiorxivApiFallback) {
15151603
try {
@@ -1717,6 +1805,58 @@ const VFB_QUERY_SHORT_NAMES = [
17171805
{ name: 'ImagesThatDevelopFrom', description: 'List images of neurons that develop from $NAME' }
17181806
]
17191807

1808+
function escapeRegexForPattern(value = '') {
1809+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
1810+
}
1811+
1812+
const VFB_QUERY_SHORT_NAME_MAP = new Map(
1813+
VFB_QUERY_SHORT_NAMES.map(entry => [entry.name.toLowerCase(), entry.name])
1814+
)
1815+
1816+
const VFB_QUERY_SHORT_NAME_REGEX = new RegExp(
1817+
`\\b(?:${VFB_QUERY_SHORT_NAMES.map(entry => escapeRegexForPattern(entry.name)).join('|')})\\b`,
1818+
'gi'
1819+
)
1820+
1821+
const VFB_CANONICAL_ID_REGEX = /\b(?:FBbt_\d{8}|VFB_\d{8}|FBgn\d{7}|FBal\d{7}|FBti\d{7}|FBco\d{7}|FBst\d{7})\b/i
1822+
const RUN_QUERY_PREPARATION_TOOL_NAMES = new Set([
1823+
'vfb_search_terms',
1824+
'vfb_get_term_info',
1825+
'vfb_resolve_entity',
1826+
'vfb_resolve_combination'
1827+
])
1828+
1829+
function extractRequestedVfbQueryShortNames(message = '') {
1830+
if (!message) return []
1831+
1832+
const matches = message.match(VFB_QUERY_SHORT_NAME_REGEX) || []
1833+
const canonicalMatches = matches
1834+
.map(match => VFB_QUERY_SHORT_NAME_MAP.get(match.toLowerCase()))
1835+
.filter(Boolean)
1836+
1837+
return Array.from(new Set(canonicalMatches))
1838+
}
1839+
1840+
function hasCanonicalVfbOrFlybaseId(message = '') {
1841+
return VFB_CANONICAL_ID_REGEX.test(message)
1842+
}
1843+
1844+
function isStandaloneQueryTypeDirective(message = '', requestedQueryTypes = []) {
1845+
if (!message || requestedQueryTypes.length === 0) return false
1846+
1847+
let residual = message.toLowerCase()
1848+
for (const queryType of requestedQueryTypes) {
1849+
residual = residual.replace(new RegExp(`\\b${escapeRegexForPattern(queryType)}\\b`, 'gi'), ' ')
1850+
}
1851+
1852+
residual = residual
1853+
.replace(/\b(vfb_run_query|run_query|run query|use|please|can|could|you|tool|tools|query|queries|for|with|the|a|an|and|or|this|that|now|show|me)\b/gi, ' ')
1854+
.replace(/[^a-z0-9_]+/gi, ' ')
1855+
.trim()
1856+
1857+
return residual.length === 0
1858+
}
1859+
17201860
function buildVfbQueryLinkSkill() {
17211861
const queryLines = VFB_QUERY_SHORT_NAMES
17221862
.map(({ name, description }) => `- ${name}: ${description}`)
@@ -2012,7 +2152,7 @@ Use these results to continue. If more tools are needed, send another JSON tool
20122152
}
20132153

20142154
function hasExplicitVfbRunQueryRequest(message = '') {
2015-
return /\bvfb_run_query\b/i.test(message)
2155+
return /\b(vfb_run_query|run_query|run query)\b/i.test(message)
20162156
}
20172157

20182158
function hasConnectivityIntent(message = '') {
@@ -2023,7 +2163,9 @@ function buildToolPolicyCorrectionMessage({
20232163
userMessage = '',
20242164
explicitRunQueryRequested = false,
20252165
connectivityIntent = false,
2026-
missingRunQueryExecution = false
2166+
missingRunQueryExecution = false,
2167+
requestedQueryTypes = [],
2168+
hasCanonicalIdInUserMessage = false
20272169
}) {
20282170
const policyBullets = [
20292171
'- Choose the smallest set of tools that best answers the user request.',
@@ -2038,6 +2180,15 @@ function buildToolPolicyCorrectionMessage({
20382180
policyBullets.push('- The user explicitly asked for vfb_run_query, so include a plan that leads to vfb_run_query.')
20392181
}
20402182

2183+
if (requestedQueryTypes.length > 0) {
2184+
const queryList = requestedQueryTypes.join(', ')
2185+
policyBullets.push(`- The user explicitly requested query type${requestedQueryTypes.length > 1 ? 's' : ''}: ${queryList}. Preserve these exact query_type values when calling vfb_run_query.`)
2186+
policyBullets.push('- Resolve target term(s), then use vfb_get_term_info + vfb_run_query. Do not substitute vfb_query_connectivity for this request unless the user asks for class-to-class dataset comparison.')
2187+
if (!hasCanonicalIdInUserMessage) {
2188+
policyBullets.push('- If the target term is ambiguous, ask one short clarifying question instead of starting broad exploratory tool loops.')
2189+
}
2190+
}
2191+
20412192
if (connectivityIntent) {
20422193
policyBullets.push('- This is a connectivity-style request; favor VFB connectivity/query tools over docs-only search.')
20432194
}
@@ -2435,7 +2586,9 @@ async function processResponseStream({
24352586
const accumulatedItems = []
24362587
const maxToolRounds = 10
24372588
const maxToolPolicyCorrections = 3
2438-
const explicitRunQueryRequested = hasExplicitVfbRunQueryRequest(userMessage)
2589+
const requestedQueryTypes = extractRequestedVfbQueryShortNames(userMessage)
2590+
const explicitRunQueryRequested = hasExplicitVfbRunQueryRequest(userMessage) || requestedQueryTypes.length > 0
2591+
const hasCanonicalIdInUserMessage = hasCanonicalVfbOrFlybaseId(userMessage)
24392592
const connectivityIntent = hasConnectivityIntent(userMessage)
24402593
const collectedGraphSpecs = []
24412594
let currentResponse = apiResponse
@@ -2506,8 +2659,13 @@ async function processResponseStream({
25062659

25072660
if (requestedToolCalls.length > 0) {
25082661
const hasVfbToolCall = requestedToolCalls.some(toolCall => toolCall.name.startsWith('vfb_'))
2662+
const hasVfbRunQueryToolCall = requestedToolCalls.some(toolCall => toolCall.name === 'vfb_run_query')
2663+
const hasRunQueryPreparationCall = requestedToolCalls.some(toolCall => RUN_QUERY_PREPARATION_TOOL_NAMES.has(toolCall.name))
2664+
const hasConnectivityComparisonCall = requestedToolCalls.some(toolCall => toolCall.name === 'vfb_query_connectivity')
25092665
const shouldCorrectToolChoice = toolPolicyCorrections < maxToolPolicyCorrections && (
2510-
explicitRunQueryRequested && !hasVfbToolCall
2666+
(explicitRunQueryRequested && !hasVfbToolCall) ||
2667+
(explicitRunQueryRequested && !hasVfbRunQueryToolCall && !hasRunQueryPreparationCall) ||
2668+
(requestedQueryTypes.length > 0 && hasConnectivityComparisonCall && !hasVfbRunQueryToolCall)
25112669
)
25122670

25132671
if (shouldCorrectToolChoice) {
@@ -2522,7 +2680,9 @@ async function processResponseStream({
25222680
content: buildToolPolicyCorrectionMessage({
25232681
userMessage,
25242682
explicitRunQueryRequested,
2525-
connectivityIntent
2683+
connectivityIntent,
2684+
requestedQueryTypes,
2685+
hasCanonicalIdInUserMessage
25262686
})
25272687
})
25282688

@@ -2642,7 +2802,9 @@ async function processResponseStream({
26422802
userMessage,
26432803
explicitRunQueryRequested,
26442804
connectivityIntent,
2645-
missingRunQueryExecution: true
2805+
missingRunQueryExecution: true,
2806+
requestedQueryTypes,
2807+
hasCanonicalIdInUserMessage
26462808
})
26472809
})
26482810

@@ -2842,7 +3004,7 @@ export async function POST(request) {
28423004
const body = await request.json()
28433005
const messages = Array.isArray(body.messages) ? body.messages : []
28443006
const scene = body.scene || {}
2845-
const message = typeof messages[messages.length - 1]?.content === 'string'
3007+
let message = typeof messages[messages.length - 1]?.content === 'string'
28463008
? messages[messages.length - 1].content
28473009
: ''
28483010

@@ -2915,6 +3077,38 @@ export async function POST(request) {
29153077
return createImmediateErrorResponse(refusalMessage, requestId, responseId)
29163078
}
29173079

3080+
const requestedQueryTypes = extractRequestedVfbQueryShortNames(message)
3081+
if (requestedQueryTypes.length > 0 && isStandaloneQueryTypeDirective(message, requestedQueryTypes)) {
3082+
const recentUserContext = messages
3083+
.slice(0, -1)
3084+
.reverse()
3085+
.find(item => item?.role === 'user' && typeof item?.content === 'string' && item.content.trim().length > 0)
3086+
?.content
3087+
?.trim()
3088+
3089+
if (recentUserContext) {
3090+
message = `${message}\n\nUse this most recent user context as the target term scope: "${recentUserContext}".`
3091+
} else {
3092+
const responseId = `local-${requestId}`
3093+
const clarificationMessage = `I can run ${requestedQueryTypes.join(', ')}, but I need a target term label or ID first (for example "medulla" or "FBbt_00003748").`
3094+
3095+
await finalizeGovernanceEvent({
3096+
requestId,
3097+
responseId,
3098+
clientIp,
3099+
startTime,
3100+
rateCheck,
3101+
message,
3102+
responseText: clarificationMessage,
3103+
blockedRequestedDomains,
3104+
refusal: false,
3105+
reasonCode: 'query_target_required'
3106+
})
3107+
3108+
return createImmediateResultResponse(clarificationMessage, requestId, responseId)
3109+
}
3110+
}
3111+
29183112
loadLookupCache()
29193113

29203114
return buildSseResponse(async (sendEvent) => {

0 commit comments

Comments
 (0)