@@ -124,6 +124,8 @@ def _parse_csv_env(name: str, default: str) -> list[str]:
124124CHAT_HISTORY_MAX_MESSAGES = max (2 , int (os .getenv ("CHAT_HISTORY_MAX_MESSAGES" , "6" )))
125125CHAT_HISTORY_TRUNCATED_CHARS = max (200 , int (os .getenv ("CHAT_HISTORY_TRUNCATED_CHARS" , "600" )))
126126CHAT_HISTORY_FULL_TAIL_MESSAGES = max (2 , int (os .getenv ("CHAT_HISTORY_FULL_TAIL_MESSAGES" , "4" )))
127+ MATCH_PHRASE_MAX_WINDOW = max (2 , int (os .getenv ("MATCH_PHRASE_MAX_WINDOW" , "6" )))
128+ CHAT_BOOK_QUOTA_PER_BOOK = max (1 , int (os .getenv ("CHAT_BOOK_QUOTA_PER_BOOK" , "2" )))
127129
128130
129131def _compute_vector_source_fingerprint (text_limit : int ) -> tuple [Optional [int ], Optional [str ]]:
@@ -320,7 +322,7 @@ def _segment_text_tokens(text: str) -> list[str]:
320322 return re .findall (r"[A-Za-z0-9+\-./]+|[\u0370-\u03ff]+|[\u4e00-\u9fff]+" , normalized )
321323
322324
323- def _build_token_phrases (tokens : list [str ], max_window : int = 4 ) -> set [str ]:
325+ def _build_token_phrases (tokens : list [str ], max_window : int = MATCH_PHRASE_MAX_WINDOW ) -> set [str ]:
324326 phrases = set ()
325327 token_count = len (tokens )
326328 for start in range (token_count ):
@@ -346,9 +348,7 @@ def _concept_matches_text(concept: str, normalized_text: str, phrase_set: set[st
346348 if not concept :
347349 return False
348350 if _contains_chinese (concept ):
349- if concept in phrase_set :
350- return True
351- return len (concept ) >= 4 and concept in normalized_text
351+ return concept in phrase_set
352352 return bool (_compile_non_cjk_term_pattern (concept ).search (normalized_text ))
353353
354354
@@ -921,6 +921,55 @@ def _fetch_ai_gaokao_rows_for_terms(con, terms: list[str], limit: int) -> list[d
921921 return rows [:limit ]
922922
923923
924+ def _apply_chat_book_diversity (rows : list [dict ], * , limit : int , quota_per_book : int = CHAT_BOOK_QUOTA_PER_BOOK ) -> list [dict ]:
925+ if not rows :
926+ return []
927+
928+ selected = []
929+ selected_ids = set ()
930+ per_book_counts = Counter ()
931+ grouped_rows : dict [str , list [dict ]] = {}
932+ for row in rows :
933+ book_key = row .get ("book_key" ) or f"id:{ row .get ('id' )} "
934+ grouped_rows .setdefault (book_key , []).append (row )
935+
936+ # Pass 1: maximize book diversity first.
937+ for book_rows in grouped_rows .values ():
938+ row = book_rows [0 ]
939+ selected .append (row )
940+ selected_ids .add (row .get ("id" ))
941+ book_key = row .get ("book_key" ) or f"id:{ row .get ('id' )} "
942+ per_book_counts [book_key ] += 1
943+ if len (selected ) >= limit :
944+ return selected [:limit ]
945+
946+ # Pass 2: fill remaining slots while respecting the per-book quota.
947+ for book_key , book_rows in grouped_rows .items ():
948+ for row in book_rows [1 :]:
949+ if row .get ("id" ) in selected_ids :
950+ continue
951+ if per_book_counts [book_key ] >= quota_per_book :
952+ continue
953+ selected .append (row )
954+ selected_ids .add (row .get ("id" ))
955+ per_book_counts [book_key ] += 1
956+ if len (selected ) >= limit :
957+ break
958+ if len (selected ) >= limit :
959+ break
960+
961+ # Pass 3: if we still don't have enough, allow overflow rows back in.
962+ if len (selected ) < limit :
963+ for row in rows :
964+ if row .get ("id" ) in selected_ids :
965+ continue
966+ selected .append (row )
967+ selected_ids .add (row .get ("id" ))
968+ if len (selected ) >= limit :
969+ break
970+ return selected [:limit ]
971+
972+
924973def _fetch_chat_rows_for_terms (con , terms : list [str ], * , source : str , limit : int ):
925974 rows = []
926975 existing_ids = set ()
@@ -940,7 +989,7 @@ def _fetch_chat_rows_for_terms(con, terms: list[str], *, source: str, limit: int
940989 existing_ids .add (row_id )
941990
942991 rows .sort (key = lambda item : (item ["rank" ], item .get ("_term_index" , 0 ), item ["id" ]))
943- return rows [: limit ]
992+ return _apply_chat_book_diversity ( rows , limit = limit )
944993
945994
946995def _build_chat_context_payload (con , query : str , user_message : str , history : list [dict ] | None = None ) -> dict :
@@ -968,8 +1017,9 @@ def _build_chat_context_payload(con, query: str, user_message: str, history: lis
9681017 groups = []
9691018 evidence = []
9701019 for subject , subject_rows in sorted (by_subject .items (), key = lambda item : len (item [1 ]), reverse = True )[:4 ]:
1020+ diverse_rows = _apply_chat_book_diversity (subject_rows , limit = max (2 , len (subject_rows )))
9711021 selected = []
972- for row in subject_rows [:2 ]:
1022+ for row in diverse_rows [:2 ]:
9731023 logical_page = row ["logical_page" ] if row ["logical_page" ] is not None else row ["section" ]
9741024 snippet = _compose_chunk_snippet (row .get ("ai_summary" ), row .get ("text" ), limit = 180 )
9751025 citation = f"[{ subject } ·{ row ['title' ]} ·p{ logical_page } ]"
0 commit comments