Spaces:
Sleeping
Sleeping
File size: 5,301 Bytes
8191051 f02dea5 8191051 71f07fc 8191051 71f07fc 8191051 71f07fc 8191051 71f07fc 8191051 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | import re
import logging
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Patterns for output cleanup
# ---------------------------------------------------------------------------
# Strips raw [THINK]...[/THINK] blocks from visible output
_THINK_PATTERN = re.compile(r'\[THINK\](.*?)\[/THINK\]', re.DOTALL)
# Strips the internal [RAG_EMPTY] signal tag if it leaks into output
_RAG_EMPTY_PATTERN = re.compile(r'\[RAG_EMPTY\].*?\[/RAG_EMPTY\]', re.DOTALL)
# Matches "N/A", "Unknown", "Missing", "None" (case-insensitive) when used as values
_NA_VALUE_PATTERN = re.compile(
r'(?:rating|price|cost|cuisine|review|stars?|score)\s*[::]\s*(?:N/A|Unknown|Missing|None|-)\b',
re.IGNORECASE
)
# Matches standalone known POI markers that are NOT yet wrapped in [[...]]
# We rely on the model to wrap them; this is a safety net for bare uppercase-starting words
# preceded by known location/attraction indicators.
_POI_INDICATOR = re.compile(
r'(?<!\[)\[(?!\[)([A-ZÁÀẢÃẠĂẮẰẲẴẶÂẤẦẨẪẬÉÈẺẼẸÊẾỀỂỄỆÍÌỈĨỊÓÒỎÕỌÔỐỒỔỖỘƠỚỜỞỠỢÚÙỦŨỤƯỨỪỬỮỰÝỲỶỸỴĐ][^\[\]]{2,60})\](?!\])',
re.UNICODE
)
# "Based on general travel information..." prefix for empty-RAG responses
_GENERAL_INFO_PREFIX = {
"vi": "Dựa trên thông tin du lịch chung",
"en": "Based on general travel information",
"ja": "一般的な旅行情報に基づくと",
"ko": "일반적인 여행 정보에 따르면",
}
def parse_llm_response(raw_output: str, language: str = "en", rag_was_empty: bool = False) -> dict:
"""Parse and post-process LLM output.
Steps:
1. Extract [THINK] reasoning blocks (hidden from user).
2. Remove internal control tags.
3. Strip N/A / Unknown / Missing value strings.
4. Enforce [[POI]] bracket format on any single-bracketed references.
5. Prepend "Based on general travel information..." when RAG was empty.
6. Clean up whitespace.
Args:
raw_output: Raw text from the LLM.
language: Response language code (vi/en/ja/ko).
rag_was_empty: Whether the RAG context was empty for this query.
Returns:
dict with keys:
- text: User-facing response text
- reasoning: Chain-of-thought reasoning (or None)
- rag_was_empty: Passed-through flag for downstream use
"""
if not raw_output or not raw_output.strip():
return {"text": "", "reasoning": None, "rag_was_empty": rag_was_empty}
# 1. Extract [THINK] blocks
think_matches = _THINK_PATTERN.findall(raw_output)
reasoning = "\n".join(m.strip() for m in think_matches) if think_matches else None
# 2. Remove [THINK] and [RAG_EMPTY] tags
visible_text = _THINK_PATTERN.sub('', raw_output)
visible_text = _RAG_EMPTY_PATTERN.sub('', visible_text)
# 3. Remove N/A value patterns
visible_text = _NA_VALUE_PATTERN.sub('', visible_text)
# 4. Fix single-bracket POI references → [[POI]]
# e.g. [Hội An Ancient Town] → [[Hội An Ancient Town]]
visible_text = _POI_INDICATOR.sub(r'[[\1]]', visible_text)
# 5. Prepend general-info prefix when RAG was empty and model hasn't already added it
if rag_was_empty:
prefix = _GENERAL_INFO_PREFIX.get(language, _GENERAL_INFO_PREFIX["en"])
if prefix.lower() not in visible_text.lower():
visible_text = f"{prefix} —\n\n{visible_text}"
# 6. Normalise whitespace
visible_text = re.sub(r'\n{3,}', '\n\n', visible_text)
visible_text = visible_text.strip()
return {
"text": visible_text,
"reasoning": reasoning,
"rag_was_empty": rag_was_empty,
}
def extract_quick_replies_from_text(text: str, language: str = "vi") -> list[str]:
"""Attempt to extract suggested follow-up questions from LLM output.
If the LLM includes numbered suggestions or bullet points at the end,
extract them as quick replies.
"""
lines = text.strip().split('\n')
suggestions = []
# Look for patterns like "1. ...", "- ...", "• ..." at the end of text
for line in reversed(lines):
line = line.strip()
match = re.match(r'^(?:\d+[.)]\s*|[-•]\s*)(.+)$', line)
if match and len(match.group(1)) < 60:
suggestions.insert(0, match.group(1).strip())
elif suggestions:
break
# Only return if we found a reasonable number
if 2 <= len(suggestions) <= 6:
return suggestions
return []
def validate_response(text: str, intent: str, min_length: int = 10) -> bool:
"""Basic validation of LLM response quality.
Args:
text: Generated response text
intent: Expected intent
min_length: Minimum response length
Returns:
True if response passes validation
"""
if not text or len(text.strip()) < min_length:
return False
# Check for obvious garbage (repeated characters, mostly special chars)
if len(set(text)) < 5:
return False
# Check repetition ratio
words = text.split()
if len(words) > 10:
unique_ratio = len(set(words)) / len(words)
if unique_ratio < 0.2:
return False
return True
|