ProfillyBot / src /response_enhancer.py
MinhDS's picture
Deploy ProfillyBot: Gradio ZeroGPU + CV RAG (Qwen2.5-3B)
0828c2c verified
Raw
History Blame Contribute Delete
10.3 kB
"""Response enhancement module for improving RAG outputs."""
import logging
import re
from .config_loader import get_config
logger = logging.getLogger(__name__)
class ResponseEnhancer:
"""Enhance and polish RAG responses for better user experience."""
def __init__(self):
"""Initialize response enhancer with configuration."""
self.config = get_config()
self.name = self.config.get("profile.name", "the candidate")
# Negative phrases to detect and rewrite
self.negative_patterns = [
(
r"(?:However|Unfortunately|I apologize),?\s*(?:I\s+)?don'?t\s+have\s+(?:any\s+)?(?:more\s+)?(?:specific\s+)?(?:information|details)\s+(?:about|on|regarding)\s+([^.!?]+)",
self._rewrite_no_info,
),
(
r"(?:I\s+)?(?:don'?t|do\s+not)\s+have\s+(?:any\s+)?(?:more\s+)?(?:information|details|data)\s+(?:about|on|regarding)\s+([^.!?]+)",
self._rewrite_no_info,
),
(
r"(?:I'm|I am)\s+(?:not\s+)?(?:un)?(?:sure|certain|aware)\s+(?:about|of)\s+([^.!?]+)",
self._rewrite_uncertain,
),
(
r"(?:I\s+)?(?:cannot|can'?t|unable to)\s+(?:provide|share|give)\s+(?:more\s+)?(?:information|details)\s+(?:about|on)\s+([^.!?]+)",
self._rewrite_cannot_provide,
),
]
# Closing enhancements (more modest)
self.positive_closings = [
"For more details, connecting directly would be helpful.",
"Additional information can be discussed in a direct conversation.",
"Feel free to reach out for more specific information.",
"Direct contact would provide more comprehensive details.",
]
def _rewrite_no_info(self, match: re.Match) -> str:
"""Rewrite 'don't have information' statements.
Args:
match: Regex match object
Returns:
Rewritten positive statement
"""
topic = match.group(1).strip()
rewrites = [
f"For more specific details about {topic}, it would be best to connect directly.",
"The available profile focuses on other aspects of the background.",
f"Additional information about {topic} can be discussed in a direct conversation.",
"The documented profile covers the key highlights.",
]
# Use hash to consistently select same rewrite for same topic
idx = hash(topic) % len(rewrites)
return rewrites[idx]
def _rewrite_uncertain(self, match: re.Match) -> str:
"""Rewrite uncertain statements.
Args:
match: Regex match object
Returns:
Rewritten confident statement
"""
topic = match.group(1).strip()
rewrites = [
f"Specific details about {topic} would be best discussed directly.",
f"For more information about {topic}, connecting directly would be helpful.",
f"Details about {topic} may be available through direct conversation.",
]
idx = hash(topic) % len(rewrites)
return rewrites[idx]
def _rewrite_cannot_provide(self, match: re.Match) -> str:
"""Rewrite 'cannot provide' statements.
Args:
match: Regex match object
Returns:
Rewritten forward-looking statement
"""
topic = match.group(1).strip()
rewrites = [
f"More detailed information about {topic} can be discussed directly.",
f"For additional context about {topic}, direct conversation would be helpful.",
f"Further details about {topic} are available through direct contact.",
]
idx = hash(topic) % len(rewrites)
return rewrites[idx]
def _add_positive_closing(self, text: str) -> str:
"""Add a modest closing statement if appropriate.
Args:
text: Response text
Returns:
Text with closing added if needed (only for very negative endings)
"""
# Check if response already mentions connecting or reaching out
forward_looking_patterns = [
r"(?:connect|contact|reach out|discuss|conversation|directly)",
r"(?:feel free|available|happy|welcome)\s+to",
]
for pattern in forward_looking_patterns:
if re.search(pattern, text, re.IGNORECASE):
return text # Already has some form of closing or contact suggestion
# Only add closing for clearly negative endings
very_negative_ending_patterns = [
r"(?:however|unfortunately).*(?:don'?t|cannot|can'?t|no)[^.!?]*[.!?]\s*$",
r"(?:not\s+available|not\s+found|no\s+information)[^.!?]*[.!?]\s*$",
]
for pattern in very_negative_ending_patterns:
if re.search(pattern, text, re.IGNORECASE):
# Add a modest closing
closing = self.positive_closings[hash(text) % len(self.positive_closings)]
return f"{text.rstrip()} {closing}" # Single space, not double newline
return text
def _fix_markdown_formatting(self, text: str) -> str:
"""Fix markdown formatting issues, particularly inline numbered lists.
Args:
text: Response text with potential formatting issues
Returns:
Text with properly formatted markdown
"""
# Simple but effective approach: Replace inline list patterns with line-broken versions
# Step 1: Fix inline numbered lists
# Pattern: Find any text that has "1. text 2. text" (not already on separate lines)
# We'll use a simple replace: put newline before each " N. " where N is a digit
# But first, protect already well-formatted lists (those at start of line)
lines = text.split("\n")
protected_lines = []
for line in lines:
stripped = line.strip()
# Check if line starts with a number (already formatted list)
if re.match(r"^\d+\.\s+", stripped):
# Protect by temporarily marking it
protected_lines.append("___LISTITEM___" + line)
else:
protected_lines.append(line)
text = "\n".join(protected_lines)
# Now fix inline lists: add line break before each " 1.", " 2.", etc.
# But only if it's in the middle of text (has non-whitespace before it)
text = re.sub(r"([^\n])\s+(\d+)\.\s+([A-Z])", r"\1\n\n\2. \3", text)
# Remove protection markers
text = text.replace("___LISTITEM___", "")
# Step 2: Ensure proper blank lines between elements
lines = text.split("\n")
formatted_lines = []
prev_was_list = False
for line in lines:
stripped = line.strip()
if not stripped:
# Don't add multiple blank lines in a row
if formatted_lines and formatted_lines[-1] != "":
formatted_lines.append("")
continue
is_list = bool(re.match(r"^\d+\.|^-\s+|^\*\s+", stripped))
# Add blank line transitions
if formatted_lines:
last_line = formatted_lines[-1]
# Add blank before first list item
if (
is_list
and not prev_was_list
and last_line != ""
or not is_list
and prev_was_list
and last_line != ""
):
formatted_lines.append("")
formatted_lines.append(line)
prev_was_list = is_list
return "\n".join(formatted_lines)
def enhance(self, response: str) -> str:
"""Enhance response by removing negative language and adding positive tone.
Args:
response: Original response text
Returns:
Enhanced response text
"""
if not response or len(response.strip()) == 0:
return response
enhanced = response
# Apply negative pattern rewrites
for pattern, rewrite_func in self.negative_patterns:
matches = list(re.finditer(pattern, enhanced, re.IGNORECASE))
# Process matches in reverse to maintain string positions
for match in reversed(matches):
replacement = rewrite_func(match)
enhanced = enhanced[: match.start()] + replacement + enhanced[match.end() :]
logger.debug(f"Rewrote negative phrase: '{match.group(0)}' -> '{replacement}'")
# Add positive closing if needed
enhanced = self._add_positive_closing(enhanced)
# Fix markdown formatting
enhanced = self._fix_markdown_formatting(enhanced)
# Clean up any double spaces or awkward punctuation
enhanced = re.sub(r" +", " ", enhanced) # Multiple spaces to single space
enhanced = re.sub(r"\s+([.,!?])", r"\1", enhanced)
enhanced = re.sub(r"([.!?])\s*([.!?])", r"\1", enhanced)
return enhanced.strip()
def enhance_with_context(self, response: str, question: str) -> str:
"""Enhance response with awareness of the question context.
Args:
response: Original response text
question: Original question
Returns:
Context-aware enhanced response
"""
enhanced = self.enhance(response)
# If the question is about job search/roles and response seems incomplete
# Add a modest closing about opportunities (not overly enthusiastic)
if re.search(
r"(?:job|role|position|opportunity|looking for|seeking)", question, re.IGNORECASE
) and not re.search(
r"(?:opportunity|interview|connect|discuss|reach out|contact)", enhanced, re.IGNORECASE
):
enhanced += (
" For current opportunities and detailed discussions, direct contact would be best."
)
return enhanced
def get_response_enhancer() -> ResponseEnhancer:
"""Get response enhancer instance.
Returns:
ResponseEnhancer instance
"""
return ResponseEnhancer()