rag-lite-qa-system / question_parser.py
Ak47-model-ml's picture
Upload 9 files
c8c05d6 verified
Raw
History Blame Contribute Delete
6.45 kB
"""
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]