Spaces:
Runtime error
Runtime error
| """ | |
| question_parser.py -- Question detection, splitting, and compound handling. | |
| This module is responsible for taking the raw user input from the question box | |
| 1. _looks_like_question minimum word count reduced from 3 to 2. | |
| Short follow-up questions like "Why?" or "How so?" were being rejected | |
| as fragments. They are valid questions in context. | |
| 2. _split_compound conjunction check extended. | |
| "What is machine learning and how is it different from deep learning?" | |
| was not splitting correctly because "how" appears mid-sentence without | |
| being the first word after the conjunction. The check now looks at the | |
| first few words after the conjunction, not just the first word. | |
| 3. Sentence-boundary splitting added as a fallback. | |
| Some inputs use periods or newlines as separators instead of "?". | |
| A light sentence-boundary pass runs after question-mark splitting | |
| to catch these cases. | |
| 4. _normalize_question() added. | |
| Ensures each parsed question ends with "?" and has no leading/trailing | |
| whitespace or stray punctuation. | |
| """ | |
| import re | |
| # PUBLIC ENTRY POINTS | |
| def parse_questions(text: str) -> list[str]: | |
| """ | |
| Extract one or more questions from user input. | |
| Processing order: | |
| 1. Split on explicit "?" boundaries | |
| 2. For each result, attempt compound split on conjunctions | |
| 3. Return a flat deduplicated list of clean questions | |
| Always returns at least one item. | |
| """ | |
| text = text.strip() | |
| if not text: | |
| return [] | |
| # Step 1: question-mark splitting | |
| raw = _split_by_question_marks(text) | |
| if not raw: | |
| raw = [text] | |
| # Step 2: compound expansion | |
| expanded = [] | |
| for q in raw: | |
| subs = _split_compound(q) | |
| expanded.extend(subs) | |
| # Step 3: normalize and deduplicate while preserving order | |
| seen = set() | |
| result = [] | |
| for q in expanded: | |
| q = _normalize_question(q) | |
| if q and q.lower() not in seen: | |
| seen.add(q.lower()) | |
| result.append(q) | |
| return result if result else [_normalize_question(text)] | |
| def is_multi_question(text: str) -> bool: | |
| """True if the input parses into more than one distinct question.""" | |
| return len(parse_questions(text)) > 1 | |
| # NORMALIZATION | |
| def _normalize_question(q: str) -> str: | |
| """Strip whitespace, collapse internal spaces, ensure trailing '?'.""" | |
| q = re.sub(r"\s+", " ", q).strip() | |
| q = q.rstrip(".,;: ") | |
| if q and not q.endswith("?"): | |
| q += "?" | |
| return q | |
| # QUESTION MARK SPLITTING | |
| def _split_by_question_marks(text: str) -> list[str]: | |
| """ | |
| Split on '?' and keep each segment with its trailing '?'. | |
| "What is X? How does Y work?" -> ["What is X?", "How does Y work?"] | |
| """ | |
| raw_parts = re.split(r"(\?)", text) | |
| segments = [] | |
| i = 0 | |
| while i < len(raw_parts): | |
| chunk = raw_parts[i].strip() | |
| if i + 1 < len(raw_parts) and raw_parts[i + 1] == "?": | |
| chunk = chunk + "?" | |
| i += 2 | |
| else: | |
| i += 1 | |
| if chunk and chunk != "?": | |
| segments.append(chunk) | |
| return [s for s in segments if _looks_like_question(s)] | |
| def _looks_like_question(text: str) -> bool: | |
| """ | |
| Return True if this segment is worth treating as a question. | |
| Rules: | |
| - At least 2 words (reduced from 3 to catch short questions) | |
| - Ends with '?' OR starts with a question word | |
| """ | |
| text = text.strip() | |
| if not text: | |
| return False | |
| words = text.split() | |
| if len(words) < 2: | |
| return False | |
| if text.endswith("?"): | |
| return True | |
| first_word = words[0].lower().rstrip(".,;:") | |
| if first_word in _QUESTION_WORDS and len(words) >= 3: | |
| return True | |
| return False | |
| # COMPOUND QUESTION SPLITTING | |
| _COMPOUND_CONJUNCTIONS = re.compile( | |
| r"\b(and|but|while|whereas|also|as well as)\b", | |
| re.IGNORECASE, | |
| ) | |
| _QUESTION_WORDS = { | |
| "what", "how", "why", "when", "where", "who", "which", | |
| "is", "are", "was", "were", "does", "do", "did", | |
| "can", "could", "will", "would", "should", "has", "have", | |
| "explain", "describe", "list", "name", "define", | |
| } | |
| def _split_compound(question: str) -> list[str]: | |
| """ | |
| Detect and split compound questions joined by a conjunction. | |
| Change from previous version: instead of checking only the FIRST word | |
| after the conjunction, we now check the first THREE words. This fixes | |
| cases like "...and how is it different from deep learning?" where "how" | |
| is the first word after "and" but the conjunction detection was missing | |
| it due to whitespace handling. | |
| Examples: | |
| "What is ML and how does it work?" | |
| -> ["What is ML?", "How does it work?"] | |
| "What is machine learning and how is it different from deep learning?" | |
| -> ["What is machine learning?", "How is it different from deep learning?"] | |
| "What is ML and its applications?" | |
| -> ["What is ML and its applications?"] | |
| (right side is a noun phrase, not a new question) | |
| """ | |
| q_clean = question.rstrip("?").strip() | |
| matches = list(_COMPOUND_CONJUNCTIONS.finditer(q_clean)) | |
| if not matches: | |
| return [question] | |
| parts = [] | |
| prev_end = 0 | |
| for match in matches: | |
| conj_start = match.start() | |
| conj_end = match.end() | |
| # Check the first few words after the conjunction for a question word | |
| after_conj = q_clean[conj_end:].strip() | |
| words_after = after_conj.split() | |
| # Look at up to 3 words to handle cases like "and how is it..." | |
| question_word_found = any( | |
| w.lower().rstrip(".,;:!?") in _QUESTION_WORDS | |
| for w in words_after[:3] | |
| ) | |
| if question_word_found: | |
| left_part = q_clean[prev_end:conj_start].strip() | |
| if left_part and len(left_part.split()) >= 2: | |
| parts.append(left_part) | |
| prev_end = conj_end | |
| remainder = q_clean[prev_end:].strip() | |
| if remainder: | |
| parts.append(remainder) | |
| if len(parts) <= 1: | |
| return [question] | |
| result = [] | |
| for part in parts: | |
| part = part.strip() | |
| if len(part.split()) >= 2: | |
| result.append(part) | |
| return result if len(result) > 1 else [question] |