@@ -207,14 +207,14 @@ function classifyTopicCategory(message = '', toolUsage = {}) {
207207 return 'publications'
208208 }
209209
210- if ( toolUsage . vfb_run_query || / \b ( c o n n e c t i v i t y | c o n n e c t o m e | s y n a p s e | s y n a p t i c | n b l a s t | p r o j e c t i o n | p r e s y n a p t i c | p o s t s y n a p t i c ) \b / i. test ( lowerMessage ) ) {
211- return 'connectivity'
212- }
213-
214210 if ( / \b ( g e n e | e x p r e s s i o n | t r a n s g e n e | g a l 4 | d r i v e r | r e p o r t e r ) \b / i. test ( lowerMessage ) ) {
215211 return 'gene expression'
216212 }
217213
214+ if ( toolUsage . vfb_run_query || / \b ( c o n n e c t i v i t y | c o n n e c t o m e | s y n a p s e | s y n a p t i c | n b l a s t | p r o j e c t i o n | p r e s y n a p t i c | p o s t s y n a p t i c ) \b / i. test ( lowerMessage ) ) {
215+ return 'connectivity'
216+ }
217+
218218 if ( / \b ( i m a g e | i m a g e s | t h u m b n a i l | p i c t u r e | p i c t u r e s | v i s u a l i [ s z ] e | 3 d | s c e n e ) \b / i. test ( lowerMessage ) ) {
219219 return 'images'
220220 }
@@ -320,7 +320,7 @@ async function getVfbMcpClient() {
320320
321321 const transport = new StreamableHTTPClientTransport ( new URL ( VFB_MCP_URL ) )
322322 const client = new Client (
323- { name : 'vfb-chat-client' , version : '3.2.2 ' } ,
323+ { name : 'vfb-chat-client' , version : '3.2.3 ' } ,
324324 { capabilities : { } }
325325 )
326326 await client . connect ( transport )
@@ -333,7 +333,7 @@ async function getBiorxivMcpClient() {
333333
334334 const transport = new StreamableHTTPClientTransport ( new URL ( BIORXIV_MCP_URL ) )
335335 const client = new Client (
336- { name : 'vfb-chat-biorxiv' , version : '3.2.2 ' } ,
336+ { name : 'vfb-chat-biorxiv' , version : '3.2.3 ' } ,
337337 { capabilities : { } }
338338 )
339339 await client . connect ( transport )
@@ -866,6 +866,7 @@ TOOL ECONOMY:
866866- Prefer the fewest tool steps needed to produce a useful answer.
867867- Do not keep calling tools just to exhaustively enumerate large result sets.
868868- If the question is broad or combinatorial, stop once you have enough evidence to give a partial answer.
869+ - For broad gene-expression or transgene-pattern requests, prefer a short representative list (about 3-5 items) and ask how the user wants to narrow further instead of trying to enumerate everything in one turn.
869870- If the question is broad or underspecified, it is good to ask 1-3 short clarifying questions instead of trying to enumerate everything immediately.
870871- When stopping early, clearly summarize what you found so far and end with 2-4 direct clarifying questions the user can answer to narrow the query (for example: one dataset, one transmitter class, one neuron subtype, one brain region, or a capped number of results).
871872
@@ -1001,37 +1002,34 @@ async function readResponseStream(apiResponse, sendEvent) {
10011002 return { textAccumulator, functionCalls, responseId, failed, errorMessage }
10021003}
10031004
1004- async function requestToolLimitSummary ( {
1005+ async function requestNoToolFallbackResponse ( {
10051006 sendEvent,
10061007 conversationInput,
10071008 accumulatedItems,
1009+ partialAssistantText = '' ,
10081010 apiBaseUrl,
10091011 apiKey,
10101012 apiModel,
10111013 outboundAllowList,
10121014 toolUsage,
10131015 toolRounds,
1014- maxToolRounds ,
1015- userMessage
1016+ statusMessage ,
1017+ instruction
10161018} ) {
1017- if ( accumulatedItems . length === 0 ) return null
1019+ sendEvent ( 'status' , { message : statusMessage , phase : 'llm' } )
10181020
1019- sendEvent ( 'status' , { message : 'Summarizing partial results' , phase : 'llm' } )
1020-
1021- const summaryInstruction = `The original user request was:
1022- "${ userMessage } "
1023-
1024- The request hit the tool-round limit after ${ toolRounds } rounds (current budget: ${ maxToolRounds } ).
1021+ const fallbackInput = [
1022+ ...conversationInput ,
1023+ ...accumulatedItems
1024+ ]
10251025
1026- Using only the gathered tool outputs already provided in this conversation:
1027- - give the best partial answer you can
1028- - clearly say that the answer is partial because the request branched into too many tool steps
1029- - summarize the strongest findings you already have
1030- - end with 2-4 direct clarification questions the user can answer so you can continue in a narrower, lower-tool way
1026+ if ( partialAssistantText . trim ( ) ) {
1027+ fallbackInput . push ( { role : 'assistant' , content : partialAssistantText . trim ( ) } )
1028+ }
10311029
1032- Do not call tools. Do not ask to browse the web.`
1030+ fallbackInput . push ( { role : 'user' , content : instruction } )
10331031
1034- const summaryResponse = await fetch ( `${ apiBaseUrl } /responses` , {
1032+ const fallbackResponse = await fetch ( `${ apiBaseUrl } /responses` , {
10351033 method : 'POST' ,
10361034 headers : {
10371035 'Content-Type' : 'application/json' ,
@@ -1040,21 +1038,17 @@ Do not call tools. Do not ask to browse the web.`
10401038 body : JSON . stringify ( {
10411039 model : apiModel ,
10421040 instructions : systemPrompt ,
1043- input : [
1044- ...conversationInput ,
1045- ...accumulatedItems ,
1046- { role : 'user' , content : summaryInstruction }
1047- ] ,
1041+ input : fallbackInput ,
10481042 tools : [ ] ,
10491043 stream : true
10501044 } )
10511045 } )
10521046
1053- if ( ! summaryResponse . ok ) {
1047+ if ( ! fallbackResponse . ok ) {
10541048 return null
10551049 }
10561050
1057- const { textAccumulator, responseId, failed } = await readResponseStream ( summaryResponse , sendEvent )
1051+ const { textAccumulator, responseId, failed } = await readResponseStream ( fallbackResponse , sendEvent )
10581052 if ( failed || ! textAccumulator ) {
10591053 return null
10601054 }
@@ -1068,10 +1062,54 @@ Do not call tools. Do not ask to browse the web.`
10681062 } )
10691063}
10701064
1065+ async function requestToolLimitSummary ( {
1066+ sendEvent,
1067+ conversationInput,
1068+ accumulatedItems,
1069+ apiBaseUrl,
1070+ apiKey,
1071+ apiModel,
1072+ outboundAllowList,
1073+ toolUsage,
1074+ toolRounds,
1075+ maxToolRounds,
1076+ userMessage
1077+ } ) {
1078+ if ( accumulatedItems . length === 0 ) return null
1079+
1080+ const summaryInstruction = `The original user request was:
1081+ "${ userMessage } "
1082+
1083+ The request hit the tool-round limit after ${ toolRounds } rounds (current budget: ${ maxToolRounds } ).
1084+
1085+ Using only the gathered tool outputs already provided in this conversation:
1086+ - give the best partial answer you can
1087+ - clearly say that the answer is partial because the request branched into too many tool steps
1088+ - summarize the strongest findings you already have
1089+ - end with 2-4 direct clarification questions the user can answer so you can continue in a narrower, lower-tool way
1090+
1091+ Do not call tools. Do not ask to browse the web.`
1092+
1093+ return requestNoToolFallbackResponse ( {
1094+ sendEvent,
1095+ conversationInput,
1096+ accumulatedItems,
1097+ apiBaseUrl,
1098+ apiKey,
1099+ apiModel,
1100+ outboundAllowList,
1101+ toolUsage,
1102+ toolRounds,
1103+ statusMessage : 'Summarizing partial results' ,
1104+ instruction : summaryInstruction
1105+ } )
1106+ }
1107+
10711108async function requestClarifyingFollowUp ( {
10721109 sendEvent,
10731110 conversationInput,
10741111 accumulatedItems,
1112+ partialAssistantText = '' ,
10751113 apiBaseUrl,
10761114 apiKey,
10771115 apiModel,
@@ -1081,8 +1119,6 @@ async function requestClarifyingFollowUp({
10811119 userMessage,
10821120 reason
10831121} ) {
1084- sendEvent ( 'status' , { message : 'Clarifying next step' , phase : 'llm' } )
1085-
10861122 const clarificationInstruction = `The original user request was:
10871123"${ userMessage } "
10881124
@@ -1096,40 +1132,65 @@ Using only the existing conversation and any tool outputs already provided:
10961132
10971133Do not call tools. Do not ask to browse the web.`
10981134
1099- const clarificationResponse = await fetch ( `${ apiBaseUrl } /responses` , {
1100- method : 'POST' ,
1101- headers : {
1102- 'Content-Type' : 'application/json' ,
1103- ...( apiKey ? { 'Authorization' : `Bearer ${ apiKey } ` } : { } )
1104- } ,
1105- body : JSON . stringify ( {
1106- model : apiModel ,
1107- instructions : systemPrompt ,
1108- input : [
1109- ...conversationInput ,
1110- ...accumulatedItems ,
1111- { role : 'user' , content : clarificationInstruction }
1112- ] ,
1113- tools : [ ] ,
1114- stream : true
1115- } )
1135+ return requestNoToolFallbackResponse ( {
1136+ sendEvent,
1137+ conversationInput,
1138+ accumulatedItems,
1139+ partialAssistantText,
1140+ apiBaseUrl,
1141+ apiKey,
1142+ apiModel,
1143+ outboundAllowList,
1144+ toolUsage,
1145+ toolRounds,
1146+ statusMessage : 'Clarifying next step' ,
1147+ instruction : clarificationInstruction
11161148 } )
1149+ }
11171150
1118- if ( ! clarificationResponse . ok ) {
1119- return null
1120- }
1151+ async function requestStreamFailureRecovery ( {
1152+ sendEvent,
1153+ conversationInput,
1154+ accumulatedItems,
1155+ partialAssistantText = '' ,
1156+ apiBaseUrl,
1157+ apiKey,
1158+ apiModel,
1159+ outboundAllowList,
1160+ toolUsage,
1161+ toolRounds,
1162+ userMessage,
1163+ reason
1164+ } ) {
1165+ if ( accumulatedItems . length === 0 && ! partialAssistantText . trim ( ) ) return null
11211166
1122- const { textAccumulator, responseId, failed } = await readResponseStream ( clarificationResponse , sendEvent )
1123- if ( failed || ! textAccumulator ) {
1124- return null
1125- }
1167+ const recoveryInstruction = `The original user request was:
1168+ "${ userMessage } "
11261169
1127- return buildSuccessfulTextResult ( {
1128- responseText : textAccumulator ,
1129- responseId,
1170+ The previous attempt ended unexpectedly before a stable final answer was produced.
1171+ Reason: ${ reason } .
1172+
1173+ Using only the existing conversation, any tool outputs already provided, and any partial answer text already shown above:
1174+ - give the best partial answer you can
1175+ - if the evidence is still too incomplete, say that briefly and ask 2-4 short clarifying questions
1176+ - prefer a short concrete answer over more questions if the available evidence already supports one
1177+ - do not invent missing facts
1178+
1179+ Do not call tools. Do not ask to browse the web.`
1180+
1181+ return requestNoToolFallbackResponse ( {
1182+ sendEvent,
1183+ conversationInput,
1184+ accumulatedItems,
1185+ partialAssistantText,
1186+ apiBaseUrl,
1187+ apiKey,
1188+ apiModel,
1189+ outboundAllowList,
11301190 toolUsage,
11311191 toolRounds,
1132- outboundAllowList
1192+ statusMessage : 'Recovering partial answer' ,
1193+ instruction : recoveryInstruction
11331194 } )
11341195}
11351196
@@ -1155,6 +1216,25 @@ async function processResponseStream({
11551216 if ( responseId ) latestResponseId = responseId
11561217
11571218 if ( failed ) {
1219+ const recovery = await requestStreamFailureRecovery ( {
1220+ sendEvent,
1221+ conversationInput,
1222+ accumulatedItems,
1223+ partialAssistantText : textAccumulator ,
1224+ apiBaseUrl,
1225+ apiKey,
1226+ apiModel,
1227+ outboundAllowList,
1228+ toolUsage,
1229+ toolRounds,
1230+ userMessage,
1231+ reason : errorMessage || 'The AI service returned an unexpected stream error.'
1232+ } )
1233+
1234+ if ( recovery ) {
1235+ return recovery
1236+ }
1237+
11581238 return {
11591239 ok : false ,
11601240 responseId : latestResponseId ,
@@ -1247,6 +1327,7 @@ async function processResponseStream({
12471327 sendEvent,
12481328 conversationInput,
12491329 accumulatedItems,
1330+ partialAssistantText : textAccumulator ,
12501331 apiBaseUrl,
12511332 apiKey,
12521333 apiModel,
0 commit comments