1414import time
1515from datetime import datetime
1616from typing import Any , Dict , List , Optional , Tuple
17+ from urllib .parse import unquote
1718
19+ from openviking .core .namespace import is_session_uri , uri_parts
1820from openviking .core .retrieval_targets import default_target_directories
1921from openviking .models .embedder .base import EmbedResult , embed_compat
2022from openviking .models .rerank import RerankClient
3638
3739logger = get_logger (__name__ )
3840
41+ _INTERNAL_SESSION_LOG_FILENAMES = frozenset ({"messages.jsonl" })
42+ _INTERNAL_SESSION_LOG_SIDECAR_FILENAMES = frozenset ({".abstract.md" , ".overview.md" })
43+ _SESSION_LOG_FILTER_OVERFETCH_FACTOR = 4
44+ _SESSION_LOG_FILTER_MAX_DECODE_PASSES = 3
45+
3946
4047class RetrieverMode (str ):
4148 THINKING = "thinking"
4249 QUICK = "quick"
4350
4451
52+ def _is_internal_session_log_uri (uri : str ) -> bool :
53+ """Return True for internal session transcript files that should not surface in recall."""
54+ normalized = uri .split ("?" , 1 )[0 ].split ("#" , 1 )[0 ].rstrip ("/" )
55+ scheme = "viking://"
56+ if not normalized .casefold ().startswith (scheme ):
57+ return False
58+
59+ path = normalized [len (scheme ) :]
60+ for _ in range (_SESSION_LOG_FILTER_MAX_DECODE_PASSES ):
61+ decoded = unquote (path )
62+ if decoded == path :
63+ break
64+ path = decoded
65+ path = path .split ("?" , 1 )[0 ].split ("#" , 1 )[0 ]
66+ normalized_uri = f"{ scheme } { path .rstrip ('/' ).casefold ()} "
67+ if not is_session_uri (normalized_uri ):
68+ return False
69+
70+ parts = uri_parts (normalized_uri )
71+ root_depth = _session_root_depth (parts )
72+ if root_depth is None or len (parts ) <= root_depth :
73+ return False
74+
75+ tail = parts [root_depth :]
76+ return _is_internal_session_log_tail (tail )
77+
78+
79+ def _session_root_depth (parts : List [str ]) -> Optional [int ]:
80+ if len (parts ) >= 2 and parts [0 ] == "session" :
81+ return 2
82+ if len (parts ) >= 3 and parts [:2 ] == ["user" , "sessions" ]:
83+ return 3
84+ if len (parts ) >= 4 and parts [0 ] == "user" and parts [2 ] == "sessions" :
85+ return 4
86+ return None
87+
88+
89+ def _is_internal_session_log_tail (tail : List [str ]) -> bool :
90+ if _is_session_transcript_tail (tail ):
91+ return True
92+ if len (tail ) >= 3 and tail [0 ] == "history" and tail [1 ].startswith ("archive_" ):
93+ return _is_session_transcript_tail (tail [2 :])
94+ return False
95+
96+
97+ def _is_session_transcript_tail (tail : List [str ]) -> bool :
98+ if tail == ["messages.jsonl" ] or tail in (
99+ [".abstract.md" ],
100+ [".overview.md" ],
101+ ):
102+ return True
103+ if (
104+ len (tail ) == 2
105+ and tail [0 ] in _INTERNAL_SESSION_LOG_FILENAMES
106+ and tail [1 ] in _INTERNAL_SESSION_LOG_SIDECAR_FILENAMES
107+ ):
108+ return True
109+ return False
110+
111+
112+ def _session_log_filtered_fetch_limit (limit : int , minimum : int ) -> int :
113+ return max (limit * _SESSION_LOG_FILTER_OVERFETCH_FACTOR , minimum )
114+
115+
116+ async def _search_in_tenant_excluding_session_logs (
117+ vector_proxy : VikingDBManagerProxy ,
118+ * ,
119+ desired_limit : int ,
120+ page_limit : int ,
121+ query_vector : Optional [List [float ]],
122+ sparse_query_vector : Optional [Dict [str , float ]],
123+ context_type : Optional [str ],
124+ target_directories : List [str ],
125+ extra_filter : Optional [FilterExpr | Dict [str , Any ]],
126+ level : Optional [List [int ]],
127+ max_scan_pages : int ,
128+ ) -> tuple [List [Dict [str , Any ]], int , int ]:
129+ results : List [Dict [str , Any ]] = []
130+ searches = 0
131+ scanned = 0
132+ offset = 0
133+ for _ in range (max_scan_pages ):
134+ page = await vector_proxy .search_in_tenant (
135+ query_vector = query_vector ,
136+ sparse_query_vector = sparse_query_vector ,
137+ context_type = context_type ,
138+ target_directories = target_directories ,
139+ extra_filter = extra_filter ,
140+ level = level ,
141+ limit = page_limit ,
142+ offset = offset ,
143+ )
144+ searches += 1
145+ scanned += len (page )
146+ results .extend (
147+ result
148+ for result in page
149+ if not _is_internal_session_log_uri (str (result .get ("uri" , "" )))
150+ )
151+ if len (results ) >= desired_limit or len (page ) < page_limit :
152+ break
153+ offset += page_limit
154+ return results [:desired_limit ], searches , scanned
155+
156+
157+ async def _search_children_excluding_session_logs (
158+ vector_proxy : VikingDBManagerProxy ,
159+ * ,
160+ parent_uri : str ,
161+ desired_limit : int ,
162+ page_limit : int ,
163+ query_vector : Optional [List [float ]],
164+ sparse_query_vector : Optional [Dict [str , float ]],
165+ context_type : Optional [str ],
166+ target_directories : Optional [List [str ]],
167+ extra_filter : Optional [FilterExpr | Dict [str , Any ]],
168+ max_scan_pages : int ,
169+ ) -> tuple [List [Dict [str , Any ]], int , int ]:
170+ results : List [Dict [str , Any ]] = []
171+ searches = 0
172+ scanned = 0
173+ offset = 0
174+ for _ in range (max_scan_pages ):
175+ page = await vector_proxy .search_children_in_tenant (
176+ parent_uri = parent_uri ,
177+ query_vector = query_vector ,
178+ sparse_query_vector = sparse_query_vector ,
179+ context_type = context_type ,
180+ target_directories = target_directories ,
181+ extra_filter = extra_filter ,
182+ limit = page_limit ,
183+ offset = offset ,
184+ )
185+ searches += 1
186+ scanned += len (page )
187+ results .extend (
188+ result
189+ for result in page
190+ if not _is_internal_session_log_uri (str (result .get ("uri" , "" )))
191+ )
192+ if len (results ) >= desired_limit or len (page ) < page_limit :
193+ break
194+ offset += page_limit
195+ return results [:desired_limit ], searches , scanned
196+
197+
45198class HierarchicalRetriever :
46199 """Hierarchical retriever with dense and sparse vector support."""
47200
@@ -73,6 +226,9 @@ def __init__(
73226 self .retrieval_config = retrieval_config or RetrievalConfig ()
74227 self .hotness_alpha = self .retrieval_config .hotness_alpha
75228 self .score_propagation_alpha = self .retrieval_config .score_propagation_alpha
229+ self .session_log_filter_max_scan_pages = (
230+ self .retrieval_config .session_log_filter_max_scan_pages
231+ )
76232
77233 # Use rerank threshold if available, otherwise use a default
78234 self .threshold = rerank_config .threshold if rerank_config else 0
@@ -151,19 +307,27 @@ async def retrieve(
151307 context_type = query .context_type .value if query .context_type else None
152308
153309 if mode == RetrieverMode .QUICK :
310+ quick_fetch_limit = _session_log_filtered_fetch_limit (limit , self .GLOBAL_SEARCH_TOPK )
154311 with telemetry .measure ("search.vector_retrieval" ):
155- quick_results = await vector_proxy .search_in_tenant (
312+ (
313+ quick_results ,
314+ search_count ,
315+ scanned_count ,
316+ ) = await _search_in_tenant_excluding_session_logs (
317+ vector_proxy ,
318+ desired_limit = max (limit , self .GLOBAL_SEARCH_TOPK ),
319+ page_limit = quick_fetch_limit ,
156320 query_vector = query_vector ,
157321 sparse_query_vector = sparse_query_vector ,
158322 context_type = context_type ,
159323 target_directories = target_dirs ,
160324 extra_filter = scope_dsl ,
161325 level = level ,
162- limit = max ( limit , self .GLOBAL_SEARCH_TOPK ) ,
326+ max_scan_pages = self .session_log_filter_max_scan_pages ,
163327 )
164- telemetry .count ("vector.searches" , 1 )
165- telemetry .count ("vector.scored" , len ( quick_results ) )
166- telemetry .count ("vector.scanned" , len ( quick_results ) )
328+ telemetry .count ("vector.searches" , search_count )
329+ telemetry .count ("vector.scored" , scanned_count )
330+ telemetry .count ("vector.scanned" , scanned_count )
167331
168332 collected_by_uri : Dict [str , Dict [str , Any ]] = {}
169333 for result in quick_results :
@@ -192,19 +356,27 @@ async def retrieve(
192356 rerank_used = False
193357 else :
194358 # Step 2: Global vector search to supplement starting points
359+ global_fetch_limit = max (limit , self .GLOBAL_SEARCH_TOPK )
195360 with telemetry .measure ("search.vector_retrieval" ):
196- global_results = await vector_proxy .search_in_tenant (
361+ (
362+ global_results ,
363+ search_count ,
364+ scanned_count ,
365+ ) = await _search_in_tenant_excluding_session_logs (
366+ vector_proxy ,
367+ desired_limit = global_fetch_limit ,
368+ page_limit = global_fetch_limit ,
197369 query_vector = query_vector ,
198370 sparse_query_vector = sparse_query_vector ,
199371 context_type = context_type ,
200372 target_directories = target_dirs ,
201373 extra_filter = scope_dsl ,
202374 level = [0 , 1 ],
203- limit = max ( limit , self .GLOBAL_SEARCH_TOPK ) ,
375+ max_scan_pages = self .session_log_filter_max_scan_pages ,
204376 )
205- telemetry .count ("vector.searches" , 1 )
206- telemetry .count ("vector.scored" , len ( global_results ) )
207- telemetry .count ("vector.scanned" , len ( global_results ) )
377+ telemetry .count ("vector.searches" , search_count )
378+ telemetry .count ("vector.scored" , scanned_count )
379+ telemetry .count ("vector.scanned" , scanned_count )
208380
209381 # Debug: Print all URIs in global_results
210382 if logger .isEnabledFor (logging .DEBUG ):
@@ -389,7 +561,7 @@ async def _recursive_search(
389561 if initial_candidates :
390562 for r in initial_candidates :
391563 uri = r .get ("uri" , "" )
392- if not uri :
564+ if not uri or _is_internal_session_log_uri ( uri ) :
393565 continue
394566 if level is None or r .get ("level" , 2 ) in level :
395567 score = self ._finite_score (r .get ("_score" , 0.0 ))
@@ -408,17 +580,25 @@ async def _recursive_search(
408580
409581 # Initialize: process starting points
410582 for uri , score in starting_points :
583+ if _is_internal_session_log_uri (uri ):
584+ continue
411585 heapq .heappush (dir_queue , (- score , uri ))
412586
413- async def search_children (current_uri : str ) -> List [Dict [str , Any ]]:
414- return await vector_proxy .search_children_in_tenant (
587+ child_desired_limit = max (limit * 2 , 20 )
588+ child_fetch_limit = _session_log_filtered_fetch_limit (limit , 20 )
589+
590+ async def search_children (current_uri : str ) -> tuple [List [Dict [str , Any ]], int , int ]:
591+ return await _search_children_excluding_session_logs (
592+ vector_proxy ,
415593 parent_uri = current_uri ,
594+ desired_limit = child_desired_limit ,
595+ page_limit = child_fetch_limit ,
416596 query_vector = query_vector ,
417- sparse_query_vector = sparse_query_vector , # Pass sparse vector
597+ sparse_query_vector = sparse_query_vector ,
418598 context_type = context_type ,
419599 target_directories = target_dirs ,
420600 extra_filter = scope_dsl ,
421- limit = max ( limit * 2 , 20 ) ,
601+ max_scan_pages = self . session_log_filter_max_scan_pages ,
422602 )
423603
424604 parallelism = max (1 , self .MAX_PARALLEL_CHILD_SEARCHES )
@@ -442,10 +622,12 @@ async def search_children(current_uri: str) -> List[Dict[str, Any]]:
442622 )
443623
444624 telemetry = get_current_telemetry ()
445- for (_ , current_score ), results in zip (batch , batch_results , strict = True ):
446- telemetry .count ("vector.searches" , 1 )
447- telemetry .count ("vector.scored" , len (results ))
448- telemetry .count ("vector.scanned" , len (results ))
625+ for (_ , current_score ), (results , search_count , scanned_count ) in zip (
626+ batch , batch_results , strict = True
627+ ):
628+ telemetry .count ("vector.searches" , search_count )
629+ telemetry .count ("vector.scored" , scanned_count )
630+ telemetry .count ("vector.scanned" , scanned_count )
449631
450632 if not results :
451633 continue
@@ -457,6 +639,8 @@ async def search_children(current_uri: str) -> List[Dict[str, Any]]:
457639
458640 for r , score in zip (results , query_scores , strict = True ):
459641 uri = r .get ("uri" , "" )
642+ if not uri or _is_internal_session_log_uri (uri ):
643+ continue
460644 final_score = (
461645 alpha * score + (1 - alpha ) * current_score if current_score else score
462646 )
0 commit comments