@@ -973,7 +973,7 @@ def assess_context_or_answer_simple(
973973 include_reasoning = False ,
974974 )
975975 _record_generation_usage (usage_tracker , followup )
976- return False , followup .get ("content" ) or ""
976+ return False , _sanitize_clarification_response ( followup .get ("content" ) or "" , session = session )
977977
978978 # Check if context is sufficient.
979979 content_upper = content .upper ().strip ()
@@ -985,7 +985,7 @@ def assess_context_or_answer_simple(
985985 if depth == "Short" :
986986 return True , ""
987987
988- return False , content
988+ return False , _sanitize_clarification_response ( content , session = session )
989989
990990def execute_tool (name , arguments , session = None ):
991991 """Execute a tool by name with the given arguments.
@@ -1209,17 +1209,16 @@ def _build_content_nodes(created_main_block, tool_results=None):
12091209 return []
12101210
12111211 content = str (created_main_block .get ("content" ) or "" ).strip ()
1212- if not content :
1213- return []
1214-
12151212 nodes = []
12161213
12171214 # --- 1. Split by ## headings into markdown sections -----------------------
12181215 import re as _re
1219- sections = _re .split (r"\n(?=##\s|###\s)" , content )
1220- sections = [s .strip () for s in sections if s .strip ()]
1221- if not sections :
1222- sections = [content ]
1216+ sections = []
1217+ if content :
1218+ sections = _re .split (r"\n(?=##\s|###\s)" , content )
1219+ sections = [s .strip () for s in sections if s .strip ()]
1220+ if not sections :
1221+ sections = [content ]
12231222
12241223 # --- 2. Collect media from tool_results (deduplicated by URL) ---------------
12251224 seen_urls = set ()
@@ -1275,44 +1274,49 @@ def _build_content_nodes(created_main_block, tool_results=None):
12751274 re .IGNORECASE ,
12761275 )
12771276
1278- # Split sections into those eligible for images and those that aren't.
1279- eligible_indices = []
1280- for idx , section in enumerate (sections ):
1281- first_line = section .split ("\n " , 1 )[0 ].strip ()
1282- if _IMAGE_ONLY_HEADING .match (first_line ):
1283- continue # drop image-only sections entirely
1284- if _CONCLUSION_HEADING .match (first_line ):
1285- sections [idx ] = section # keep but don't attach images
1286- else :
1287- eligible_indices .append (idx )
1288-
1289- # Remove image-only sections from the list.
1290- sections = [s for s in sections if not _IMAGE_ONLY_HEADING .match (s .split ("\n " , 1 )[0 ].strip ())]
1291- # Recompute eligible indices after removal.
1292- eligible_indices = []
1293- for idx , section in enumerate (sections ):
1294- first_line = section .split ("\n " , 1 )[0 ].strip ()
1295- if not _CONCLUSION_HEADING .match (first_line ):
1296- eligible_indices .append (idx )
1297-
1298- max_media = max (len (eligible_indices ) * 2 , 1 ) # up to 2 images per section
1299- media_items = media_items [:max_media ]
1300-
1301- # --- 3. Interleave markdown sections + media (up to 2 per eligible section)
1302- # Distribute media across eligible sections, skipping conclusion/preamble.
1303- media_assignment : dict [int , list ] = {} # section_index -> [media_items]
1304- for i , media in enumerate (media_items ):
1305- # Round-robin: fill each eligible section with 1, then loop back for 2nd
1306- slot = i % len (eligible_indices ) if eligible_indices else 0
1307- sec_idx = eligible_indices [slot ] if slot < len (eligible_indices ) else 0
1308- media_assignment .setdefault (sec_idx , [])
1309- if len (media_assignment [sec_idx ]) < 2 :
1310- media_assignment [sec_idx ].append (media )
1311-
1312- for idx , section in enumerate (sections ):
1313- nodes .append ({"type" : "markdown" , "content" : section })
1314- for media in media_assignment .get (idx , []):
1315- nodes .append (media )
1277+ if sections :
1278+ # Split sections into those eligible for images and those that aren't.
1279+ eligible_indices = []
1280+ for idx , section in enumerate (sections ):
1281+ first_line = section .split ("\n " , 1 )[0 ].strip ()
1282+ if _IMAGE_ONLY_HEADING .match (first_line ):
1283+ continue # drop image-only sections entirely
1284+ if _CONCLUSION_HEADING .match (first_line ):
1285+ sections [idx ] = section # keep but don't attach images
1286+ else :
1287+ eligible_indices .append (idx )
1288+
1289+ # Remove image-only sections from the list.
1290+ sections = [s for s in sections if not _IMAGE_ONLY_HEADING .match (s .split ("\n " , 1 )[0 ].strip ())]
1291+ # Recompute eligible indices after removal.
1292+ eligible_indices = []
1293+ for idx , section in enumerate (sections ):
1294+ first_line = section .split ("\n " , 1 )[0 ].strip ()
1295+ if not _CONCLUSION_HEADING .match (first_line ):
1296+ eligible_indices .append (idx )
1297+
1298+ max_media = max (len (eligible_indices ) * 2 , 1 ) # up to 2 images per section
1299+ media_items = media_items [:max_media ]
1300+
1301+ # --- 3. Interleave markdown sections + media (up to 2 per eligible section)
1302+ # Distribute media across eligible sections, skipping conclusion/preamble.
1303+ media_assignment : dict [int , list ] = {} # section_index -> [media_items]
1304+ for i , media in enumerate (media_items ):
1305+ # Round-robin: fill each eligible section with 1, then loop back for 2nd
1306+ slot = i % len (eligible_indices ) if eligible_indices else 0
1307+ sec_idx = eligible_indices [slot ] if slot < len (eligible_indices ) else 0
1308+ media_assignment .setdefault (sec_idx , [])
1309+ if len (media_assignment [sec_idx ]) < 2 :
1310+ media_assignment [sec_idx ].append (media )
1311+
1312+ for idx , section in enumerate (sections ):
1313+ nodes .append ({"type" : "markdown" , "content" : section })
1314+ for media in media_assignment .get (idx , []):
1315+ nodes .append (media )
1316+ else :
1317+ # Media-only / structured-only results are still renderable and should
1318+ # not collapse into an empty frontend block.
1319+ nodes .extend (media_items )
13161320
13171321 # --- 4. Append structured nodes -------------------------------------------
13181322 objectives = created_main_block .get ("learning_objectives" )
@@ -2131,6 +2135,12 @@ def _exec_sub(section):
21312135 )
21322136 if content_nodes :
21332137 response_block .metadata ["content_nodes" ] = content_nodes
2138+ elif not (response_block .content or "" ).strip ():
2139+ response_block .content = (
2140+ "I couldn't assemble a usable response for that request. "
2141+ "Please try again."
2142+ )
2143+ response_block .metadata ["empty_generation_fallback" ] = True
21342144
21352145 response_block .metadata ["response_kind" ] = "answer"
21362146 depth_val = (session .user_profile or {}).get ("explanation_depth" , "Medium" )
@@ -2227,6 +2237,12 @@ def _exec_sub(section):
22272237 )
22282238 if content_nodes :
22292239 response_block .metadata ["content_nodes" ] = content_nodes
2240+ elif not (response_block .content or "" ).strip ():
2241+ response_block .content = (
2242+ "I couldn't assemble a usable response for that request. "
2243+ "Please try again."
2244+ )
2245+ response_block .metadata ["empty_generation_fallback" ] = True
22302246 response_block .metadata ["response_kind" ] = "answer"
22312247 if is_main_block :
22322248 depth_val = (session .user_profile or {}).get ("explanation_depth" , "Medium" )
0 commit comments