agentic_rag / ui_helpers.py
Saint5's picture
Direct upload to ZeroGPU container
aa74750 verified
Raw
History Blame Contribute Delete
5.8 kB
"""
ui_helpers.py
---------------------
Gradio UI support functions.
_safe_content — defensive Gradio 6.x content extraction
get_history_text — formats history list for the generation prompt
build_retrieval_query — context-enriched query for follow-up questions
format_metrics — builds the right-hand metrics panel markdown
"""
from config import CONFIDENCE_THRESHOLD, MAX_PROMPT_TOKENS
def _safe_content(raw) -> str:
"""
Safely convert any Gradio message content value to a plain string.
In Gradio 6.x, 'content' can be str, list of typed blocks
(e.g. [{"type": "text", "text": "..."}]), or None. Calling
.strip() directly on a list raises AttributeError — this
function centralises the defensive conversion.
"""
if raw is None:
return ""
if isinstance(raw, str):
return raw.strip()
if isinstance(raw, list):
parts = []
for block in raw:
if isinstance(block, dict):
parts.append(block.get("text", block.get("content", "")))
elif isinstance(block, str):
parts.append(block)
return " ".join(filter(None, parts)).strip()
return str(raw).strip()
def get_history_text(history: list, max_turns: int = 3) -> str:
"""
Convert Gradio's history list to a readable conversation string
for inclusion in the generation prompt.
max_turns=3 keeps enough context to resolve references without
overflowing the prompt budget. UI status placeholders
("🔍 Searching...", etc.) are filtered out so they never leak
into the model's view of the conversation.
"""
if not history:
return ""
UI_PREFIXES = ("🔍", "📚", "🌐", "❌", "⏳")
lines = []
recent = history[-(max_turns * 2):]
for msg in recent:
if isinstance(msg, dict):
role = "User" if msg.get("role") == "user" else "Assistant"
content = _safe_content(msg.get("content"))
elif isinstance(msg, (list, tuple)) and len(msg) == 2:
for role, raw in [("User", msg[0]), ("Assistant", msg[1])]:
content = _safe_content(raw)
if content and not content.startswith(UI_PREFIXES):
lines.append(f"{role}: {content}")
continue
else:
continue
if content and not content.startswith(UI_PREFIXES):
lines.append(f"{role}: {content}")
return "\n".join(lines)
def build_retrieval_query(message: str, history: list) -> str:
"""
Enrich the FAISS/Tavily query with prior-turn context — but ONLY
when the current message looks like a genuine follow-up question.
Requires ALL three signals: short message (≤7 words), a reference
word (pronoun/article), and a question starter that presupposes
a subject. This avoids polluting retrieval for brand-new topics.
"""
if not history:
return message
words = message.lower().split()
REFERENCE_WORDS = {
"it", "they", "he", "she", "them", "those", "that",
"this", "his", "her", "its", "their", "the",
}
QUESTION_STARTERS = {
"who", "when", "where", "how", "why",
"was", "did", "is", "are",
}
is_short = len(words) <= 7
has_reference = any(w in REFERENCE_WORDS for w in words)
has_q_word = bool(words) and words[0] in QUESTION_STARTERS
if not (is_short and has_reference and has_q_word):
return message
last_user_msg = ""
for msg in reversed(history):
if isinstance(msg, dict) and msg.get("role") == "user":
last_user_msg = _safe_content(msg.get("content"))
break
elif isinstance(msg, (list, tuple)) and len(msg) >= 1:
last_user_msg = _safe_content(msg[0])
break
if last_user_msg and last_user_msg.lower() != message.lower():
return f"{last_user_msg} {message}"
return message
def format_metrics(
source_type: str = "none",
confidence: float = 0.0,
sources_count: int = 0,
prompt_tokens: int = 0,
t_retrieve: float = 0.0,
t_web: float = 0.0,
t_generate: float = 0.0,
t_total: float = 0.0,
tokens_per_sec: float = 0.0,
output_tokens: int = 0,
generating: bool = False,
elapsed: float = 0.0,
token_so_far: int = 0,
error: str = "",
) -> str:
"""
Return a markdown string for the right-hand metrics panel.
Three display modes: empty (no query yet), generating (live view
during streaming), done (full breakdown after completion).
"""
if error:
return f"❌ **Error:** `{error}`"
if source_type == "none" and not generating and t_total == 0.0:
return "_Metrics will appear here during and after the response._"
if generating:
web_line = f"- Web search: **{t_web:.1f}s**\n" if t_web > 0 else ""
return (
f"⚡ **Generating...**\n\n"
f"- Tokens out: **{token_so_far}**\n"
f"- Elapsed: **{elapsed:.1f}s**\n"
f"- Retrieval: **{t_retrieve*1000:.0f} ms**\n"
f"{web_line}"
)
source_icon = "📚" if source_type == "faiss" else "🌐"
source_name = "FAISS" if source_type == "faiss" else "Tavily"
conf_status = "✓" if confidence >= CONFIDENCE_THRESHOLD else "✗ → web"
lines = [
f"**{source_icon} {source_name}** — sim `{confidence:.3f}` {conf_status}",
"",
"**⏱ Timing**",
f"- Retrieval: `{t_retrieve * 1000:.0f} ms`",
]
if t_web > 0:
lines.append(f"- Web search: `{t_web:.2f}s`")
lines += [
f"- Generation: `{t_generate:.2f}s`",
f"- **Total: `{t_total:.2f}s`**",
"",
"**🔢 Generation**",
f"- Speed: **`{tokens_per_sec:.1f} tok/s`**",
f"- Output tokens: `{output_tokens}`",
f"- Prompt tokens: `{prompt_tokens}` / `{MAX_PROMPT_TOKENS}`",
]
if sources_count > 0:
lines.append(f"- Sources indexed: `{sources_count}`")
return "\n".join(lines)