Datavision / backend /agents /output_formatter.py
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
16.3 kB
# Executive Output Formatter - ChatGPT-Quality Responses
"""
Standardizes all AI responses to executive-ready format.
Output Structure:
1. Bold headline answer (one line)
2. Key Findings table
3. Analysis (2-3 sentences)
4. Recommendations (if applicable)
5. Footer with sources and mode
This ensures EVERY response looks professional and consistent.
"""
from typing import List, Dict, Optional, Any
from dataclasses import dataclass
from enum import Enum
class ResponseType(Enum):
"""Types of responses for formatting decisions"""
FACTUAL = "factual" # Single number/fact answer
COMPARISON = "comparison" # Comparing entities
TREND = "trend" # Time-based analysis
PREDICTION = "prediction" # Forecasts
SUMMARY = "summary" # Overview/dashboard
INSIGHT = "insight" # Analysis with recommendations
ERROR = "error" # Error or insufficient data
@dataclass
class FormattedResponse:
"""Structured response container"""
headline: str # Bold one-liner
key_findings: Optional[str] # Markdown table
analysis: Optional[str] # 2-3 sentences
recommendations: Optional[List[str]] # Bullet points
chart_payload: Optional[Dict] # Chart JSON if any
sources: List[str] # Data sources
mode: str # Analysis mode
confidence: str # HIGH/MEDIUM/LOW
def format_executive_response(
headline: str,
data_table: Optional[str] = None,
analysis: Optional[str] = None,
recommendations: Optional[List[str]] = None,
chart_payload: Optional[Dict] = None,
sources: Optional[List[str]] = None,
mode: str = "RAG",
confidence: str = "HIGH"
) -> str:
"""
Format response in ChatGPT executive style.
Always produces clean, consistent output regardless of input.
"""
parts = []
# 1. HEADLINE - Always bold, always first
parts.append(f"**{headline}**\n")
# 2. KEY FINDINGS TABLE - If data provided
if data_table and data_table.strip():
parts.append("### Key Findings")
parts.append(data_table)
parts.append("")
# 3. ANALYSIS - Brief, insightful
if analysis and analysis.strip():
parts.append("### Analysis")
parts.append(analysis)
parts.append("")
# 4. RECOMMENDATIONS - Action-oriented
if recommendations and len(recommendations) > 0:
parts.append("### Recommendations")
for rec in recommendations[:5]: # Max 5 recommendations
parts.append(f"• {rec}")
parts.append("")
# 5. CHART - If included
if chart_payload:
import json
parts.append("```plotly_chart")
parts.append(json.dumps(chart_payload, indent=2))
parts.append("```")
parts.append("")
# 6. FOOTER - Clean, minimal
footer_parts = []
if sources:
source_str = ", ".join(sources[:3]) # Max 3 sources shown
footer_parts.append(f"*Sources: {source_str}*")
footer_parts.append(f"*{mode} Mode • {confidence} Confidence*")
parts.append("---")
parts.append(" | ".join(footer_parts))
return "\n".join(parts)
def format_factual_response(
metric_name: str,
value: str,
context: Optional[str] = None,
comparison: Optional[str] = None,
sources: Optional[List[str]] = None,
mode: str = "RAG"
) -> str:
"""
Format single-fact responses (e.g., "What is total revenue?")
Keeps it concise but informative.
"""
parts = []
# Headline with the answer
parts.append(f"**{metric_name}: {value}**")
parts.append("")
# Context if available
if context:
parts.append(context)
parts.append("")
# Comparison/benchmark if available
if comparison:
parts.append(f"📊 {comparison}")
parts.append("")
# Footer
if sources:
parts.append(f"---\n*Based on {sources[0]}*")
return "\n".join(parts)
def format_comparison_response(
title: str,
comparison_table: str,
winner: Optional[str] = None,
insight: Optional[str] = None,
sources: Optional[List[str]] = None,
mode: str = "GRAPHRAG"
) -> str:
"""
Format comparison responses (e.g., "Compare customer A vs B")
"""
parts = []
# Headline
if winner:
parts.append(f"**{winner} leads in this comparison.**")
else:
parts.append(f"**{title}**")
parts.append("")
# Comparison table
parts.append(comparison_table)
parts.append("")
# Insight
if insight:
parts.append(f"💡 *{insight}*")
parts.append("")
# Footer
parts.append(f"---\n*{mode} Mode*")
return "\n".join(parts)
def format_prediction_response(
prediction_headline: str,
current_value: str,
predicted_value: str,
confidence_pct: float,
assumptions: List[str],
chart_payload: Optional[Dict] = None,
sources: Optional[List[str]] = None
) -> str:
"""
Format prediction responses with clear assumptions.
Never hide uncertainty - executives need to know confidence levels.
"""
parts = []
# Headline
parts.append(f"**{prediction_headline}**")
parts.append("")
# Prediction table
parts.append("| Metric | Value |")
parts.append("|--------|-------|")
parts.append(f"| Current | {current_value} |")
parts.append(f"| Predicted | {predicted_value} |")
parts.append(f"| Confidence | {confidence_pct:.0f}% |")
parts.append("")
# Assumptions - CRITICAL for trust
if assumptions:
parts.append("### Assumptions")
for assumption in assumptions[:4]:
parts.append(f"• {assumption}")
parts.append("")
# Confidence warning
if confidence_pct < 70:
parts.append("> ⚠️ **Low confidence prediction** - results may vary significantly")
parts.append("")
# Chart
if chart_payload:
import json
parts.append("```forecast_chart")
parts.append(json.dumps(chart_payload, indent=2))
parts.append("```")
parts.append("")
# Footer
parts.append("---")
parts.append("*PREDICTION Mode • AI-Powered Forecast*")
return "\n".join(parts)
def format_error_response(
error_type: str,
message: str,
suggestions: Optional[List[str]] = None
) -> str:
"""
Format error responses gracefully.
Never expose technical errors - always be helpful.
"""
parts = []
# Friendly headline
parts.append(f"**{error_type}**")
parts.append("")
parts.append(message)
parts.append("")
# Helpful suggestions
if suggestions:
parts.append("**What you can try:**")
for sug in suggestions:
parts.append(f"• {sug}")
return "\n".join(parts)
def format_insufficient_data_response(
query: str,
missing: List[str],
available: Optional[List[str]] = None
) -> str:
"""
When data is missing, be explicit about what's needed.
"""
parts = []
parts.append("**I don't have enough data to answer this question.**")
parts.append("")
parts.append("### What's Missing")
for item in missing:
parts.append(f"• {item}")
parts.append("")
if available:
parts.append("### What I Can Answer")
parts.append("Based on your uploaded data, I can help with:")
for item in available[:5]:
parts.append(f"• {item}")
return "\n".join(parts)
# ============================================================================
# RESPONSE CLEANER - Remove AI artifacts
# ============================================================================
def clean_ai_artifacts(response: str) -> str:
"""
🧹 ChatGPT-Level Response Cleaning v2.0
Remove common AI response artifacts:
- "As an AI..."
- "I'd be happy to..."
- "Let me analyze..."
- Planning steps ("First, I'll...")
- Reasoning preambles ("Looking at the data...")
- Excessive disclaimers
"""
import re
# Common filler phrases (at start of response)
start_fillers = [
r"(?i)^as an ai[^.]*\.\s*",
r"(?i)^i'd be happy to[^.]*\.\s*",
r"(?i)^certainly[!,.]?\s*",
r"(?i)^of course[!,.]?\s*",
r"(?i)^sure[!,.]?\s*",
r"(?i)^great question[!,.]?\s*",
r"(?i)^good question[!,.]?\s*",
r"(?i)^based on the information provided,?\s*",
r"(?i)^based on the data you've shared,?\s*",
r"(?i)^based on my analysis,?\s*",
r"(?i)^based on the uploaded data,?\s*",
]
for pattern in start_fillers:
response = re.sub(pattern, "", response)
# Planning preambles (remove entire sentences)
planning_patterns = [
r"(?i)let me analyze[^.]*\.\s*",
r"(?i)i've analyzed[^.]*\.\s*",
r"(?i)i'll start by[^.]*\.\s*",
r"(?i)first,? i'll[^.]*\.\s*",
r"(?i)first,? let me[^.]*\.\s*",
r"(?i)looking at the data[^.]*\.\s*",
r"(?i)looking at your data[^.]*\.\s*",
r"(?i)analyzing the[^.]*\.\s*",
r"(?i)to answer (this|your) question[^.]*\.\s*",
r"(?i)let me (check|look|find|see)[^.]*\.\s*",
r"(?i)i need to (first|analyze|check)[^.]*\.\s*",
r"(?i)here's what i found[^.:]*[.:]\s*",
r"(?i)here is (what|the)[^.:]*[.:]\s*",
]
for pattern in planning_patterns:
response = re.sub(pattern, "", response)
# Remove thinking/reasoning blocks
thinking_patterns = [
r"\*\*thinking\*\*:?[^*]*\*\*",
r"\*thinking\*:?[^*]*\*",
r"<thinking>.*?</thinking>",
r"\[thinking\].*?\[/thinking\]",
r"\[internal\].*?\[/internal\]",
]
for pattern in thinking_patterns:
response = re.sub(pattern, "", response, flags=re.DOTALL | re.IGNORECASE)
# Remove step-by-step planning
step_patterns = [
r"(?i)step \d+:?[^.]*\.\s*",
r"(?i)next,? i('ll|'m going to)[^.]*\.\s*",
r"(?i)now,? let me[^.]*\.\s*",
r"(?i)finally,? (i'll|let me)[^.]*\.\s*",
]
for pattern in step_patterns:
response = re.sub(pattern, "", response)
# Remove disclaimers and hedging
disclaimer_patterns = [
r"(?i)\*?note:? (that )?this (is|may be)[^*\n]*\*?\s*",
r"(?i)please note[^.]*\.\s*",
r"(?i)it's (worth|important to) (noting|note)[^.]*\.\s*",
r"(?i)keep in mind[^.]*\.\s*",
r"(?i)i should (mention|note)[^.]*\.\s*",
]
for pattern in disclaimer_patterns:
response = re.sub(pattern, "", response)
# Clean up resulting whitespace issues
response = re.sub(r'\n{3,}', '\n\n', response)
response = re.sub(r'^\s*\n', '', response)
response = response.strip()
return response
def clean_for_display(response: str, max_length: int = 3000) -> str:
"""
Full cleaning pipeline for display to user.
1. Clean AI artifacts
2. Fix formatting issues
3. Remove duplicate mode labels
4. Truncate if too long
"""
import re
# Step 1: Clean artifacts
cleaned = clean_ai_artifacts(response)
# Step 2: Remove duplicate mode labels and verbose metadata
duplicate_patterns = [
# Remove duplicate "Analysis Mode: X" lines
r'(?i)(\*\*?\s*Analysis Mode:?\s*\*?\*?:?\s*[A-Z]+\s*\*?\*?)\s*\n\s*\1',
r'(?i)✔️?\s*Analysis Mode:?\s*[A-Z]+\s*\n',
r'(?i)📊?\s*Analysis Mode:?\s*[A-Z]+\s*\n',
# Remove verbose reasoning metadata
r'(?i)(Reasoning Type:?[^\n]*\n)',
r'(?i)(Mode Weights:?[^\n]*\n)',
r'(?i)(Accuracy Tier:?[^\n]*\n)',
# Remove redundant source markers when appearing multiple times
r'(\n---\s*){2,}',
# Remove empty metadata lines
r'\n\s*---\s*\n\s*---\s*\n',
]
for pattern in duplicate_patterns:
cleaned = re.sub(pattern, '', cleaned)
# Step 3: Fix common formatting issues
cleaned = cleaned.replace(' ', ' ')
cleaned = cleaned.replace(' .', '.')
cleaned = cleaned.replace(' ,', ',')
# Step 4: Fix table alignment
lines = cleaned.split('\n')
fixed_lines = []
for line in lines:
if '|' in line and line.count('|') >= 2:
# It's a table row - ensure proper spacing
cells = line.split('|')
cells = [c.strip() for c in cells]
line = ' | '.join(cells)
fixed_lines.append(line)
cleaned = '\n'.join(fixed_lines)
# Step 5: Remove excessive newlines
cleaned = re.sub(r'\n{4,}', '\n\n\n', cleaned)
# Step 6: Truncate if needed
if len(cleaned) > max_length:
cleaned = cleaned[:max_length-3] + '...'
return cleaned.strip()
def validate_numbers_in_response(response: str, context: str) -> str:
"""
Anti-hallucination check - but DO NOT add † symbols or disclaimers.
ChatGPT-level products never add academic markers.
Just return the response as-is for clean output.
"""
# DISABLED: No more † symbols or disclaimers
# This was causing unclean output that felt "academic"
# Trust the LLM and prompt engineering instead
return response
# ============================================================================
# CONFIDENCE CALCULATOR
# ============================================================================
def calculate_response_confidence(
data_points: int,
source_count: int,
query_type: str,
has_time_series: bool = False
) -> tuple:
"""
Calculate confidence level for response.
Returns: (confidence_level: str, confidence_pct: int)
"""
score = 0
# Data points contribution (0-40 points)
if data_points >= 50:
score += 40
elif data_points >= 20:
score += 30
elif data_points >= 5:
score += 20
else:
score += 10
# Source count contribution (0-30 points)
if source_count >= 3:
score += 30
elif source_count >= 2:
score += 20
else:
score += 10
# Query type contribution (0-30 points)
if query_type in ["factual", "aggregation"]:
score += 30 # High confidence for simple queries
elif query_type in ["comparison", "trend"]:
score += 20
elif query_type in ["prediction", "causal"]:
score += 10 # Lower for predictions
# Time series bonus for trend/prediction
if has_time_series and query_type in ["trend", "prediction"]:
score += 10
# Determine level
if score >= 80:
return ("HIGH", min(score, 95))
elif score >= 60:
return ("MEDIUM", score)
elif score >= 40:
return ("LOW", score)
else:
return ("INSUFFICIENT", score)
# ============================================================================
# QUICK FORMATTERS
# ============================================================================
def format_metric_table(metrics: Dict[str, Any], currency_symbol: str = "$") -> str:
"""Create a simple metrics table"""
lines = ["| Metric | Value |", "|--------|-------|"]
for key, value in metrics.items():
if isinstance(value, (int, float)):
if value >= 1000:
formatted = f"{currency_symbol}{value:,.0f}"
else:
formatted = f"{value:,.2f}"
else:
formatted = str(value)
lines.append(f"| {key} | {formatted} |")
return "\n".join(lines)
def format_ranking_table(items: List[Dict], value_key: str, label_key: str, currency_symbol: str = "$", limit: int = 10) -> str:
"""Create a ranked table (e.g., top customers)"""
lines = ["| Rank | Name | Value |", "|------|------|-------|"]
for i, item in enumerate(items[:limit], 1):
value = item.get(value_key, 0)
label = item.get(label_key, "Unknown")
formatted_value = f"{currency_symbol}{value:,.0f}" if isinstance(value, (int, float)) else value
lines.append(f"| {i} | {label} | {formatted_value} |")
return "\n".join(lines)