@@ -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+
93106function 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+
14261473function 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 ( q u e r y [ _ \s - ] ? t y p e | i n v a l i d q u e r y | n o t a v a i l a b l e f o r t h i s i d | n o t a v a l i d q u e r y | a v a i l a b l e q u e r i e s | h t t p \s * 4 0 0 | s t a t u s c o d e 4 0 0 | b a d r e q u e s t ) \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 (?: F B b t _ \d { 8 } | V F B _ \d { 8 } | F B g n \d { 7 } | F B a l \d { 7 } | F B t i \d { 7 } | F B c o \d { 7 } | F B s t \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 ( v f b _ r u n _ q u e r y | r u n _ q u e r y | r u n q u e r y | u s e | p l e a s e | c a n | c o u l d | y o u | t o o l | t o o l s | q u e r y | q u e r i e s | f o r | w i t h | t h e | a | a n | a n d | o r | t h i s | t h a t | n o w | s h o w | m e ) \b / gi, ' ' )
1854+ . replace ( / [ ^ a - z 0 - 9 _ ] + / gi, ' ' )
1855+ . trim ( )
1856+
1857+ return residual . length === 0
1858+ }
1859+
17201860function 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
20142154function hasExplicitVfbRunQueryRequest ( message = '' ) {
2015- return / \b v f b _ r u n _ q u e r y \b / i. test ( message )
2155+ return / \b ( v f b _ r u n _ q u e r y | r u n _ q u e r y | r u n q u e r y ) \b / i. test ( message )
20162156}
20172157
20182158function 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