Spaces:
Running
Running
| """ | |
| Query Decomposer - Break complex queries into sub-queries | |
| ========================================================== | |
| Handles multi-part questions by: | |
| 1. Detecting if query is complex | |
| 2. Breaking into sub-queries | |
| 3. Merging results back together | |
| Examples: | |
| - "What is total revenue and who are top customers?" -> 2 sub-queries | |
| - "Compare Q1 vs Q2 and show trends" -> 2 sub-queries | |
| """ | |
| import re | |
| from typing import List, Dict, Tuple, Optional | |
| from dataclasses import dataclass | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| class SubQuery: | |
| """A decomposed sub-query""" | |
| query: str | |
| query_type: str # factual, aggregation, comparison, etc. | |
| priority: int = 1 | |
| depends_on: Optional[int] = None # Index of query this depends on | |
| class DecomposedQuery: | |
| """Result of query decomposition""" | |
| original: str | |
| is_complex: bool | |
| sub_queries: List[SubQuery] | |
| merge_strategy: str # 'concat', 'compare', 'summarize' | |
| def is_complex_query(query: str) -> bool: | |
| """ | |
| Detect if a query needs decomposition. | |
| Complex queries contain: | |
| - Multiple questions (and, also, then) | |
| - Comparisons (vs, versus, compare) | |
| - Multi-step requests (first...then, after that) | |
| """ | |
| query_lower = query.lower() | |
| # Multi-part indicators | |
| multi_part_patterns = [ | |
| r'\band\b.*\?', # "X and Y?" | |
| r'\balso\b', # "also show" | |
| r'\bthen\b', # "then compare" | |
| r'\bfirst\b.*\bthen\b', # "first X then Y" | |
| r'\b(?:as well|additionally|moreover)\b', | |
| ] | |
| for pattern in multi_part_patterns: | |
| if re.search(pattern, query_lower): | |
| return True | |
| # Multiple question marks | |
| if query.count('?') > 1: | |
| return True | |
| # Long queries often need decomposition | |
| if len(query.split()) > 25: | |
| return True | |
| return False | |
| def decompose_query(query: str) -> DecomposedQuery: | |
| """ | |
| Decompose a complex query into simpler sub-queries. | |
| Args: | |
| query: The original user query | |
| Returns: | |
| DecomposedQuery with sub-queries and merge strategy | |
| """ | |
| if not is_complex_query(query): | |
| return DecomposedQuery( | |
| original=query, | |
| is_complex=False, | |
| sub_queries=[SubQuery(query=query, query_type="simple")], | |
| merge_strategy="single" | |
| ) | |
| sub_queries = [] | |
| merge_strategy = "concat" | |
| # Split by common conjunctions | |
| query_lower = query.lower() | |
| # Handle "X and Y" patterns | |
| if ' and ' in query_lower: | |
| parts = re.split(r'\s+and\s+', query, flags=re.IGNORECASE) | |
| for i, part in enumerate(parts): | |
| part = part.strip().rstrip('?').strip() | |
| if len(part) > 5: # Meaningful part | |
| query_type = _detect_query_type(part) | |
| sub_queries.append(SubQuery( | |
| query=part + "?", | |
| query_type=query_type, | |
| priority=i + 1 | |
| )) | |
| # Handle comparison patterns | |
| elif any(p in query_lower for p in ['compare', ' vs ', 'versus', 'difference between']): | |
| merge_strategy = "compare" | |
| # Extract entities to compare | |
| compare_match = re.search( | |
| r'(?:compare|difference between)\s+(.+?)\s+(?:and|vs|versus|with)\s+(.+?)(?:\?|$)', | |
| query, re.IGNORECASE | |
| ) | |
| if compare_match: | |
| entity1 = compare_match.group(1).strip() | |
| entity2 = compare_match.group(2).strip() | |
| sub_queries.append(SubQuery( | |
| query=f"What are the details of {entity1}?", | |
| query_type="lookup", | |
| priority=1 | |
| )) | |
| sub_queries.append(SubQuery( | |
| query=f"What are the details of {entity2}?", | |
| query_type="lookup", | |
| priority=1 | |
| )) | |
| sub_queries.append(SubQuery( | |
| query=f"Compare {entity1} vs {entity2}", | |
| query_type="comparison", | |
| priority=2, | |
| depends_on=0 # Depends on first two | |
| )) | |
| # Handle "Compare X vs Y over time" (Trend Comparison) | |
| trend_match = re.search(r'(?:compare|how did)\s+(.+?)\s+(?:vs|versus|and)\s+(.+?)\s+(?:perform|change|grow|trend)\s+(?:over time|historically)', query_lower) | |
| if trend_match: | |
| entity1 = trend_match.group(1).strip() | |
| entity2 = trend_match.group(2).strip() | |
| sub_queries.append(SubQuery( | |
| query=f"Show trend for {entity1}", | |
| query_type="trend", | |
| priority=1 | |
| )) | |
| sub_queries.append(SubQuery( | |
| query=f"Show trend for {entity2}", | |
| query_type="trend", | |
| priority=1 | |
| )) | |
| sub_queries.append(SubQuery( | |
| query=f"Compare trends of {entity1} vs {entity2}", | |
| query_type="comparison_trend", | |
| priority=2 | |
| )) | |
| merge_strategy = "compare" | |
| # Handle "first...then" patterns | |
| elif 'first' in query_lower and 'then' in query_lower: | |
| merge_strategy = "sequential" | |
| parts = re.split(r'\s*(?:,\s*)?then\s+', query, flags=re.IGNORECASE) | |
| for i, part in enumerate(parts): | |
| part = part.replace('first', '').strip() | |
| if len(part) > 5: | |
| sub_queries.append(SubQuery( | |
| query=part, | |
| query_type=_detect_query_type(part), | |
| priority=i + 1, | |
| depends_on=i - 1 if i > 0 else None | |
| )) | |
| # Fallback: treat as single query | |
| if not sub_queries: | |
| sub_queries.append(SubQuery(query=query, query_type="complex")) | |
| return DecomposedQuery( | |
| original=query, | |
| is_complex=True, | |
| sub_queries=sub_queries, | |
| merge_strategy=merge_strategy | |
| ) | |
| def _detect_query_type(query: str) -> str: | |
| """Detect the type of a sub-query""" | |
| query_lower = query.lower() | |
| if any(w in query_lower for w in ['total', 'sum', 'count', 'how many', 'how much']): | |
| return "aggregation" | |
| elif any(w in query_lower for w in ['top', 'best', 'highest', 'lowest', 'worst']): | |
| return "ranking" | |
| elif any(w in query_lower for w in ['trend', 'over time', 'growth', 'change']): | |
| return "trend" | |
| elif any(w in query_lower for w in ['compare', 'vs', 'versus', 'difference']): | |
| return "comparison" | |
| elif any(w in query_lower for w in ['why', 'reason', 'cause', 'explain']): | |
| return "analytical" | |
| elif any(w in query_lower for w in ['list', 'show', 'give me', 'what are']): | |
| return "listing" | |
| elif any(w in query_lower for w in ['predict', 'forecast', 'will', 'future']): | |
| return "prediction" | |
| else: | |
| return "factual" | |
| def merge_results(results: List[str], strategy: str = "concat") -> str: | |
| """ | |
| Merge results from multiple sub-queries. | |
| Args: | |
| results: List of answers from sub-queries | |
| strategy: How to merge ('concat', 'compare', 'summarize') | |
| Returns: | |
| Merged answer string | |
| """ | |
| if not results: | |
| return "No results found." | |
| if len(results) == 1: | |
| return results[0] | |
| if strategy == "compare": | |
| # Format as comparison | |
| merged = "## Comparison\n\n" | |
| for i, result in enumerate(results): | |
| merged += f"**Option {i + 1}:**\n{result}\n\n" | |
| return merged | |
| elif strategy == "sequential": | |
| # Format as steps | |
| merged = "" | |
| for i, result in enumerate(results): | |
| merged += f"**Step {i + 1}:**\n{result}\n\n" | |
| return merged | |
| else: # concat | |
| return "\n\n---\n\n".join(results) | |
| def get_query_intent(query: str) -> Dict[str, any]: | |
| """ | |
| Analyze query intent for routing decisions. | |
| Returns: | |
| Dict with intent, entities, and confidence | |
| """ | |
| query_lower = query.lower() | |
| intent = { | |
| 'type': 'unknown', | |
| 'entities': [], | |
| 'confidence': 0.5, | |
| 'needs_data': True, | |
| 'needs_chart': False | |
| } | |
| # Detect chart needs | |
| if any(w in query_lower for w in ['chart', 'graph', 'plot', 'visualize', 'show me']): | |
| intent['needs_chart'] = True | |
| # Detect intent type | |
| if any(w in query_lower for w in ['hi', 'hello', 'hey']): | |
| intent['type'] = 'greeting' | |
| intent['needs_data'] = False | |
| intent['confidence'] = 0.95 | |
| elif any(w in query_lower for w in ['total', 'sum', 'how much', 'how many']): | |
| intent['type'] = 'aggregation' | |
| intent['confidence'] = 0.85 | |
| elif any(w in query_lower for w in ['top', 'best', 'highest']): | |
| intent['type'] = 'ranking' | |
| intent['confidence'] = 0.85 | |
| elif any(w in query_lower for w in ['compare', 'vs', 'difference']): | |
| intent['type'] = 'comparison' | |
| intent['confidence'] = 0.85 | |
| elif any(w in query_lower for w in ['predict', 'forecast', 'future']): | |
| intent['type'] = 'prediction' | |
| intent['confidence'] = 0.8 | |
| elif any(w in query_lower for w in ['list', 'show all', 'give me all']): | |
| intent['type'] = 'listing' | |
| intent['confidence'] = 0.8 | |
| else: | |
| intent['type'] = 'general' | |
| intent['confidence'] = 0.6 | |
| # Extract potential entities (capitalized words) | |
| entities = re.findall(r'\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\b', query) | |
| intent['entities'] = [e for e in entities if e.lower() not in | |
| ['what', 'who', 'when', 'where', 'how', 'why', 'show', 'list', 'give']] | |
| return intent | |