33"""SemanticProcessor: Processes messages from SemanticQueue, generates .abstract.md and .overview.md."""
44
55import asyncio
6+ import re
67import threading
78from contextlib import nullcontext
89from dataclasses import dataclass , field
@@ -652,11 +653,10 @@ async def _gen(idx: int, file_path: str) -> None:
652653 for file_path , summary in zip (file_paths , file_summaries , strict = False )
653654 if file_path in paths_to_vectorize and summary is not None
654655 ]
655- overview = await self ._generate_overview (
656+ generated_content = await self ._generate_overview (
656657 dir_uri , completed_summaries , [], llm_sem = llm_sem
657658 )
658- abstract = self ._extract_abstract_from_overview (overview )
659- overview , abstract = self ._enforce_size_limits (overview , abstract )
659+ overview , abstract = self ._normalize_overview_generation (generated_content )
660660
661661 try :
662662 wrote_semantics = await self ._write_memory_directory_semantics (
@@ -1155,8 +1155,49 @@ async def _generate_single_file_summary(
11551155 else :
11561156 return await self ._generate_text_summary (file_path , file_name , llm_sem , ctx = ctx )
11571157
1158+ def _replace_index_references (
1159+ self , generated_content : str , file_index_map : Dict [int , str ]
1160+ ) -> str :
1161+ def replace_index (match ):
1162+ idx = int (match .group (1 ))
1163+ return file_index_map .get (idx , match .group (0 ))
1164+
1165+ return re .sub (r"\[(\d+)\]" , replace_index , generated_content )
1166+
1167+ def _truncate_generated_text (self , text : str , max_chars : int ) -> str :
1168+ if max_chars <= 0 or len (text ) <= max_chars :
1169+ return text
1170+
1171+ if max_chars <= 3 :
1172+ return text [:max_chars ]
1173+
1174+ first_sentence_end = None
1175+ last_sentence_end_within_limit = None
1176+ for sentence_end_match in re .finditer (
1177+ r"\.(?!\d)(?=\s|$)|[!?](?=\s|$)|[。?!]" , text
1178+ ):
1179+ sentence_end = sentence_end_match .end ()
1180+ if first_sentence_end is None :
1181+ first_sentence_end = sentence_end
1182+ if sentence_end <= max_chars :
1183+ last_sentence_end_within_limit = sentence_end
1184+ elif last_sentence_end_within_limit is not None :
1185+ break
1186+
1187+ if last_sentence_end_within_limit is not None :
1188+ return text [:last_sentence_end_within_limit ].strip ()
1189+ if first_sentence_end is not None :
1190+ return text [:first_sentence_end ].strip ()
1191+
1192+ candidate = text [: max_chars - 3 ].rstrip ()
1193+ word_boundary = candidate .rfind (" " )
1194+ if word_boundary > 0 :
1195+ return candidate [:word_boundary ].rstrip () + "..."
1196+
1197+ return candidate + "..."
1198+
11581199 def _extract_abstract_from_overview (self , overview_content : str ) -> str :
1159- """Extract abstract from overview.md ."""
1200+ """Extract an abstract from the Markdown overview brief description ."""
11601201 lines = overview_content .split ("\n " )
11611202
11621203 # Skip header lines (starting with #)
@@ -1178,13 +1219,19 @@ def _extract_abstract_from_overview(self, overview_content: str) -> str:
11781219
11791220 return "\n " .join (content_lines ).strip ()
11801221
1222+ def _normalize_overview_generation (self , generated_content : str ) -> Tuple [str , str ]:
1223+ """Convert raw Markdown overview output into final L1 overview and L0 abstract."""
1224+ overview = generated_content
1225+ abstract = self ._extract_abstract_from_overview (generated_content )
1226+ return self ._enforce_size_limits (overview , abstract )
1227+
11811228 def _enforce_size_limits (self , overview : str , abstract : str ) -> Tuple [str , str ]:
11821229 """Enforce max size limits on overview and abstract."""
11831230 semantic = get_openviking_config ().semantic
11841231 if len (overview ) > semantic .overview_max_chars :
1185- overview = overview [: semantic .overview_max_chars ]
1232+ overview = self . _truncate_generated_text ( overview , semantic .overview_max_chars )
11861233 if len (abstract ) > semantic .abstract_max_chars :
1187- abstract = abstract [: semantic .abstract_max_chars - 3 ] + "..."
1234+ abstract = self . _truncate_generated_text ( abstract , semantic .abstract_max_chars )
11881235 return overview , abstract
11891236
11901237 def _parse_overview_md (self , overview_content : str ) -> Dict [str , str ]:
@@ -1247,7 +1294,7 @@ async def _generate_overview(
12471294 children_abstracts : List [Dict [str , str ]],
12481295 llm_sem : Optional [asyncio .Semaphore ] = None ,
12491296 ) -> str :
1250- """Generate directory's . overview.md (L1) .
1297+ """Generate raw directory overview model output .
12511298
12521299 For small directories, generates a single overview from all file summaries.
12531300 For large directories that would exceed the prompt budget, splits file
@@ -1260,7 +1307,7 @@ async def _generate_overview(
12601307 children_abstracts: Subdirectory summary list
12611308
12621309 Returns:
1263- Overview content
1310+ Markdown overview content generated by the model.
12641311 """
12651312
12661313 config = get_openviking_config ()
@@ -1359,9 +1406,8 @@ async def _single_generate_overview(
13591406 output_language : str = "en" ,
13601407 ) -> str :
13611408 """Generate overview from a single prompt (small directories)."""
1362- import re
1363-
1364- vlm = get_openviking_config ().vlm
1409+ config = get_openviking_config ()
1410+ vlm = config .vlm
13651411
13661412 try :
13671413 prompt = render_prompt (
@@ -1377,12 +1423,7 @@ async def _single_generate_overview(
13771423 with bind_telemetry_stage ("resource_summarize" ):
13781424 overview = await vlm .get_completion_async (prompt )
13791425
1380- # Post-process: replace [number] with actual file name
1381- def replace_index (match ):
1382- idx = int (match .group (1 ))
1383- return file_index_map .get (idx , match .group (0 ))
1384-
1385- overview = re .sub (r"\[(\d+)\]" , replace_index , overview )
1426+ overview = self ._replace_index_references (overview , file_index_map )
13861427
13871428 return overview .strip ()
13881429
@@ -1407,10 +1448,9 @@ async def _batched_generate_overview(
14071448 Splits file summaries into batches, generates a partial overview per
14081449 batch, then merges all partials into a final overview.
14091450 """
1410- import re
1411-
1412- vlm = get_openviking_config ().vlm
1413- semantic = get_openviking_config ().semantic
1451+ config = get_openviking_config ()
1452+ vlm = config .vlm
1453+ semantic = config .semantic
14141454 batch_size = semantic .overview_batch_size
14151455 dir_name = dir_uri .split ("/" )[- 1 ]
14161456
@@ -1459,19 +1499,12 @@ async def _batched_generate_overview(
14591499 )
14601500 batch_prompts .append ((batch_idx , prompt , batch_index_map ))
14611501
1462- def make_replacer (idx_map ):
1463- def replacer (match ):
1464- idx = int (match .group (1 ))
1465- return idx_map .get (idx , match .group (0 ))
1466-
1467- return replacer
1468-
14691502 async def _run_batch (batch_idx : int , prompt : str , batch_index_map : Dict [int , str ]) -> None :
14701503 try :
14711504 async with llm_sem :
14721505 with bind_telemetry_stage ("resource_summarize" ):
14731506 partial = await vlm .get_completion_async (prompt )
1474- partial = re . sub ( r"\[(\d+)\]" , make_replacer ( batch_index_map ), partial )
1507+ partial = self . _replace_index_references ( partial , batch_index_map )
14751508 partial_overviews [batch_idx ] = partial .strip ()
14761509 except Exception as e :
14771510 logger .warning (
@@ -1503,6 +1536,7 @@ async def _run_batch(batch_idx: int, prompt: str, batch_index_map: Dict[int, str
15031536 )
15041537 with bind_telemetry_stage ("resource_summarize" ):
15051538 overview = await vlm .get_completion_async (prompt )
1539+ overview = self ._replace_index_references (overview , file_index_map )
15061540 return overview .strip ()
15071541 except Exception as e :
15081542 logger .error (
0 commit comments