galbendavids's picture
Add better error handling and logging for debugging HF Spaces issues
80f29b9
"""
SQL-based Feedback Analysis Service
This module implements a SQL-based approach to analyzing feedback:
1. LLM analyzes user query
2. LLM generates 1-5 SQL queries to answer the question
3. Execute SQL queries on the feedback DataFrame
4. LLM synthesizes a comprehensive answer from query + SQL queries + results
5. (Optional) Generate visualizations of the results
"""
from __future__ import annotations
import json
import re
from dataclasses import dataclass
from typing import List, Dict, Any, Optional
import pandas as pd
import sqlite3
from .config import settings
from .data_loader import load_feedback
try:
from openai import OpenAI # type: ignore
except Exception:
OpenAI = None
try:
import google.generativeai as genai # type: ignore
except Exception:
genai = None
@dataclass
class SQLQueryResult:
"""
Result of a single SQL query execution.
Attributes:
query: The SQL query that was executed
result: DataFrame containing the query results (empty if error occurred)
error: Error message if query failed, None if successful
"""
query: str
result: pd.DataFrame
error: Optional[str] = None
@dataclass
class AnalysisResult:
"""
Complete analysis result from processing a user query.
Attributes:
user_query: The original question asked by the user
sql_queries: List of SQL queries that were generated and executed
query_results: Results from executing each SQL query
summary: Final synthesized answer in natural language
visualizations: Optional list of visualization specifications for frontend rendering
"""
user_query: str
sql_queries: List[str]
query_results: List[SQLQueryResult]
summary: str
visualizations: Optional[List[Dict[str, Any]]] = None
class SQLFeedbackService:
"""
Main service for SQL-based feedback analysis.
This service implements a 4-stage pipeline:
1. Generate SQL queries from natural language questions (using LLM)
2. Execute SQL queries on feedback data (using SQLite in-memory)
3. Synthesize comprehensive answers from query results (using LLM)
4. Generate visualization specifications for results
The service also includes automatic quality evaluation and improvement
of generated answers to ensure high-quality responses.
"""
def __init__(self):
"""
Initialize the SQL feedback service.
Loads feedback data from CSV into memory. If loading fails,
the service will still initialize but will raise errors when
trying to process queries.
"""
self.df: Optional[pd.DataFrame] = None
self._load_data()
def _load_data(self) -> None:
"""
Load feedback data from CSV file into memory.
The data is loaded once at initialization and kept in memory
for fast query execution. If the CSV file is missing or invalid,
an error is logged but the service continues to initialize.
Raises:
FileNotFoundError: If CSV file doesn't exist (handled internally)
ValueError: If CSV is missing required columns (handled internally)
"""
try:
from .config import settings
self.df = load_feedback()
csv_path_used = settings.csv_path
print(f"โœ… Loaded {len(self.df)} feedback records from: {csv_path_used}", flush=True)
except Exception as e:
print(f"โŒ Error loading feedback data: {e}", flush=True)
import traceback
traceback.print_exc()
self.df = None
def _get_schema_info(self) -> str:
"""
Generate comprehensive schema information for the feedback table.
This function analyzes the actual CSV file structure and provides
detailed information about each field including business meaning,
data types, examples, and usage patterns.
Returns:
A detailed formatted string describing the table schema with
business context, examples, and statistics.
"""
if self.df is None:
return "No data available"
# Helper function to get sample values
def get_sample_values(col_name, n=5):
try:
samples = self.df[col_name].dropna().head(n).tolist()
return [str(s) for s in samples]
except:
return []
# Helper function to get unique values if not too many
def get_unique_values(col_name, max_show=10):
try:
unique_vals = self.df[col_name].dropna().unique().tolist()
if len(unique_vals) <= max_show:
return unique_vals
return unique_vals[:max_show]
except:
return []
all_columns_info = ""
# Analyze each column that exists in the dataframe
for col in self.df.columns:
col_info = ""
# Get column statistics
dtype = str(self.df[col].dtype)
non_null_count = self.df[col].notna().sum()
null_count = self.df[col].isna().sum()
samples = get_sample_values(col, 3)
# ID field
if col == 'ID':
col_info = f"โ€ข {col} (UUID/ื˜ืงืกื˜): ืžื–ื”ื” ื™ื™ื—ื•ื“ื™ ื’ืœื•ื‘ืœื™ ืฉืœ ื›ืœ ืžืฉื•ื‘\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžื–ื”ื” ื™ื™ื—ื•ื“ื™ ืœื›ืœ ืžืฉื•ื‘ ื‘ืžืขืจื›ืช, ืžืืคืฉืจ ืžืขืงื‘, ืงื™ืฉื•ืจ ื‘ื™ืŸ ืžืฉื•ื‘ื™ื, ื•ืžื ื™ืขืช ื›ืคื™ืœื•ื™ื•ืช\n"
col_info += f" - ื“ื•ื’ืžืื•ืช: {', '.join(samples[:2])}\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE ID = '...', COUNT(DISTINCT ID), GROUP BY ID\n"
# ServiceName field
elif col == 'ServiceName':
unique_services = self.df[col].nunique()
unique_samples = get_unique_values(col, 5)
col_info = f"โ€ข {col} (ื˜ืงืกื˜): ืฉื ื”ืฉื™ืจื•ืช ื”ื“ื™ื’ื™ื˜ืœื™ ื”ืžืžืฉืœืชื™\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžื–ื”ื” ืืช ื”ืฉื™ืจื•ืช ืฉืขืœื™ื• ื ื™ืชืŸ ื”ืžืฉื•ื‘. ืžืืคืฉืจ ื ื™ืชื•ื— ืœืคื™ ืฉื™ืจื•ืช, ื”ืฉื•ื•ืื” ื‘ื™ืŸ ืฉื™ืจื•ืชื™ื, ื–ื™ื”ื•ื™ ืฉื™ืจื•ืชื™ื ื‘ืขื™ื™ืชื™ื™ื ืื• ืžืฆื˜ื™ื™ื ื™ื\n"
col_info += f" - ื™ืฉ {unique_services} ืฉื™ืจื•ืชื™ื ื™ื™ื—ื•ื“ื™ื™ื ื‘ืžืขืจื›ืช\n"
col_info += f" - ื“ื•ื’ืžืื•ืช: {', '.join(unique_samples[:3])}\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE ServiceName = '...', GROUP BY ServiceName, COUNT(*) GROUP BY ServiceName\n"
# Level field
elif col == 'Level':
level_dist = self.df[col].value_counts().sort_index().to_dict()
avg_level = self.df[col].mean()
col_info = f"โ€ข {col} (ืžืกืคืจ ืฉืœื 1-5): ื“ื™ืจื•ื’ ืฉื‘ื™ืขื•ืช ืจืฆื•ืŸ ื”ืžืฉืชืžืฉ ืžื”ืฉื™ืจื•ืช\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžื“ื“ ืฉื‘ื™ืขื•ืช ืจืฆื•ืŸ. 1=ื’ืจื•ืข ืžืื•ื“, 2=ื’ืจื•ืข, 3=ื‘ื™ื ื•ื ื™, 4=ื˜ื•ื‘, 5=ืžืขื•ืœื”. ืžืืคืฉืจ ืžื“ื™ื“ืช ืฉื‘ื™ืขื•ืช ืจืฆื•ืŸ, ื–ื™ื”ื•ื™ ื‘ืขื™ื•ืช, ื•ืžืขืงื‘ ืื—ืจ ืฉื™ืคื•ืจื™ื\n"
col_info += f" - ื“ื™ืจื•ื’ ืžืžื•ืฆืข: {avg_level:.2f}\n"
col_info += f" - ื—ืœื•ืงื”: {level_dist}\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE Level >= 4 (ืžืฉื•ื‘ื™ื ื—ื™ื•ื‘ื™ื™ื), WHERE Level <= 2 (ืžืฉื•ื‘ื™ื ืฉืœื™ืœื™ื™ื), AVG(Level), GROUP BY Level\n"
# Text field
elif col == 'Text':
sample_text = samples[0][:80] + "..." if samples and len(samples[0]) > 80 else (samples[0] if samples else "")
avg_length = self.df[col].str.len().mean() if self.df[col].dtype == 'object' else 0
col_info = f"โ€ข {col} (ื˜ืงืกื˜ ืืจื•ืš): ื”ืชื•ื›ืŸ ื”ื—ื•ืคืฉื™ ืฉืœ ื”ืžืฉื•ื‘ ืžื”ืžืฉืชืžืฉ\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ื‘ื™ืงื•ืจื•ืช, ื”ืฆืขื•ืช ืœืฉื™ืคื•ืจ, ืชืœื•ื ื•ืช, ืžื—ืžืื•ืช. ืžืืคืฉืจ ื ื™ืชื•ื— ืื™ื›ื•ืชื™, ื–ื™ื”ื•ื™ ื ื•ืฉืื™ื ื—ื•ื–ืจื™ื, ื•ืงื‘ืœืช ืชื•ื‘ื ื•ืช ืขืกืงื™ื•ืช\n"
col_info += f" - ืื•ืจืš ืžืžื•ืฆืข: {avg_length:.0f} ืชื•ื•ื™ื\n"
col_info += f" - ื“ื•ื’ืžื”: '{sample_text}'\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE Text LIKE '%ืžื™ืœื”%', WHERE Text LIKE '%ื‘ืขื™ื”%', LENGTH(Text), COUNT(*) WHERE Text IS NOT NULL\n"
# ReferenceNumber field
elif col == 'ReferenceNumber':
if non_null_count > 0:
ref_min = int(self.df[col].min())
ref_max = int(self.df[col].max())
col_info = f"โ€ข {col} (ืžืกืคืจ ืฉืœื): ืžืกืคืจ ื”ืคื ื™ื” ืคื ื™ืžื™ ืฉืœ ื”ืžืฉื•ื‘\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžืกืคืจ ื”ืคื ื™ื” ื‘ืžืขืจื›ืช. ืžืืคืฉืจ ืงื™ืฉื•ืจ ืœืžืกืžื›ื™ื ืื• ื‘ืงืฉื•ืช ืงืฉื•ืจื•ืช, ืžืขืงื‘ ืื—ืจ ืชื”ืœื™ื›ื™ื, ื•ื ื™ื”ื•ืœ ื‘ืงืฉื•ืช\n"
col_info += f" - ื˜ื•ื•ื—: {ref_min} - {ref_max}\n"
col_info += f" - ื“ื•ื’ืžืื•ืช: {', '.join([str(s) for s in samples[:2]])}\n"
col_info += f" - NULL: {null_count} ืจืฉื•ืžื•ืช ({null_count/len(self.df)*100:.1f}%)\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE ReferenceNumber = 6928, WHERE ReferenceNumber IS NOT NULL\n"
# RequestID field
elif col == 'RequestID':
if non_null_count > 0:
col_info = f"โ€ข {col} (UUID/ื˜ืงืกื˜): ืžื–ื”ื” ื™ื™ื—ื•ื“ื™ ืฉืœ ื”ื‘ืงืฉื” ื”ืžืงื•ืจื™ืช ืฉืงืฉื•ืจื” ืœืžืฉื•ื‘\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžืืคืฉืจ ืงื™ืฉื•ืจ ื‘ื™ืŸ ื‘ืงืฉื•ืช ืœืžืฉื•ื‘ื™ื, ืžืขืงื‘ ืื—ืจ ืชื”ืœื™ื›ื™ื, ื•ื ื™ืชื•ื— ื”ืงืฉืจ ื‘ื™ืŸ ื‘ืงืฉื” ืœืžืฉื•ื‘\n"
col_info += f" - ื“ื•ื’ืžืื•ืช: {samples[0][:30]}...\n"
col_info += f" - NULL: {null_count} ืจืฉื•ืžื•ืช ({null_count/len(self.df)*100:.1f}%)\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE RequestID = '...', COUNT(DISTINCT RequestID)\n"
# ProcessID field
elif col == 'ProcessID':
col_info = f"โ€ข {col} (UUID/ื˜ืงืกื˜): ืžื–ื”ื” ื™ื™ื—ื•ื“ื™ ืฉืœ ื”ืชื”ืœื™ืš ื”ืขืกืงื™ ืฉืงืฉื•ืจ ืœืžืฉื•ื‘\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžืืคืฉืจ ื ื™ืชื•ื— ืœืคื™ ืชื”ืœื™ื›ื™ื, ื–ื™ื”ื•ื™ ืชื”ืœื™ื›ื™ื ื‘ืขื™ื™ืชื™ื™ื, ื•ืžืขืงื‘ ืื—ืจ ื‘ื™ืฆื•ืขื™ื\n"
col_info += f" - NULL: {null_count} ืจืฉื•ืžื•ืช ({null_count/len(self.df)*100:.1f}%)\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE ProcessID = '...', COUNT(DISTINCT ProcessID)\n"
# Year field
elif col == 'Year':
year_min = int(self.df[col].min())
year_max = int(self.df[col].max())
year_dist = self.df[col].value_counts().sort_index().to_dict()
col_info = f"โ€ข {col} (ืžืกืคืจ ืฉืœื): ืฉื ื” ืฉื‘ื” ื ื™ืชืŸ ื”ืžืฉื•ื‘\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžืืคืฉืจ ื ื™ืชื•ื— ืžื’ืžื•ืช ืœืื•ืจืš ืฉื ื™ื, ื”ืฉื•ื•ืื” ื‘ื™ืŸ ืฉื ื™ื, ื–ื™ื”ื•ื™ ืฉื™ืคื•ืจื™ื ืื• ื”ื™ื“ืจื“ืจื•ืช, ื•ืชื›ื ื•ืŸ ืืกื˜ืจื˜ื’ื™\n"
col_info += f" - ื˜ื•ื•ื—: {year_min} - {year_max}\n"
col_info += f" - ื—ืœื•ืงื”: {year_dist}\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE Year = 2020, GROUP BY Year, SELECT Year, COUNT(*) GROUP BY Year\n"
# Month field
elif col == 'Month':
month_min = int(self.df[col].min())
month_max = int(self.df[col].max())
month_names = {1: 'ื™ื ื•ืืจ', 2: 'ืคื‘ืจื•ืืจ', 3: 'ืžืจืฅ', 4: 'ืืคืจื™ืœ', 5: 'ืžืื™', 6: 'ื™ื•ื ื™',
7: 'ื™ื•ืœื™', 8: 'ืื•ื’ื•ืกื˜', 9: 'ืกืคื˜ืžื‘ืจ', 10: 'ืื•ืงื˜ื•ื‘ืจ', 11: 'ื ื•ื‘ืžื‘ืจ', 12: 'ื“ืฆืžื‘ืจ'}
col_info = f"โ€ข {col} (ืžืกืคืจ ืฉืœื 1-12): ื—ื•ื“ืฉ ื‘ืฉื ื” ืฉื‘ื• ื ื™ืชืŸ ื”ืžืฉื•ื‘\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžืืคืฉืจ ื ื™ืชื•ื— ืขื•ื ืชื™, ื–ื™ื”ื•ื™ ื—ื•ื“ืฉื™ื ื‘ืขื™ื™ืชื™ื™ื ืื• ืžืฆื˜ื™ื™ื ื™ื, ืชื›ื ื•ืŸ ืžืฉืื‘ื™ื ืœืคื™ ืขื•ื ื•ืช\n"
col_info += f" - ื˜ื•ื•ื—: {month_min} - {month_max} ({month_names.get(month_min, '')} - {month_names.get(month_max, '')})\n"
col_info += f" - ื“ื•ื’ืžืื•ืช: {', '.join([str(s) for s in samples[:3]])}\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE Month = 1, GROUP BY Month, SELECT Month, COUNT(*) GROUP BY Month ORDER BY Month\n"
# DayInMonth field
elif col == 'DayInMonth':
day_min = int(self.df[col].min())
day_max = int(self.df[col].max())
col_info = f"โ€ข {col} (ืžืกืคืจ ืฉืœื 1-31): ื™ื•ื ื‘ื—ื•ื“ืฉ ืฉื‘ื• ื ื™ืชืŸ ื”ืžืฉื•ื‘\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžืืคืฉืจ ื ื™ืชื•ื— ืœืคื™ ื™ืžื™ื ื‘ื—ื•ื“ืฉ, ื–ื™ื”ื•ื™ ื™ืžื™ื ื‘ืขื™ื™ืชื™ื™ื (ืœืžืฉืœ ืกื•ืฃ ื—ื•ื“ืฉ), ื•ื ื™ืชื•ื— ื“ืคื•ืกื™ื ื™ื•ืžื™ื™ื\n"
col_info += f" - ื˜ื•ื•ื—: {day_min} - {day_max}\n"
col_info += f" - ื“ื•ื’ืžืื•ืช: {', '.join([str(s) for s in samples[:3]])}\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE DayInMonth = 1, GROUP BY DayInMonth, SELECT DayInMonth, COUNT(*) GROUP BY DayInMonth\n"
# DayOfWeek field
elif col == 'DayOfWeek':
unique_days = get_unique_values(col, 10)
day_names_he = {'Monday': 'ืฉื ื™', 'Tuesday': 'ืฉืœื™ืฉื™', 'Wednesday': 'ืจื‘ื™ืขื™', 'Thursday': 'ื—ืžื™ืฉื™',
'Friday': 'ืฉื™ืฉื™', 'Saturday': 'ืฉื‘ืช', 'Sunday': 'ืจืืฉื•ืŸ'}
col_info = f"โ€ข {col} (ื˜ืงืกื˜): ื™ื•ื ื‘ืฉื‘ื•ืข ืฉื‘ื• ื ื™ืชืŸ ื”ืžืฉื•ื‘ (ื‘ืื ื’ืœื™ืช)\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžืืคืฉืจ ื ื™ืชื•ื— ืœืคื™ ื™ืžื™ ื”ืฉื‘ื•ืข, ื–ื™ื”ื•ื™ ื™ืžื™ื ื‘ืขื™ื™ืชื™ื™ื, ืชื›ื ื•ืŸ ื›ื•ื— ืื“ื, ื•ื–ื™ื”ื•ื™ ื“ืคื•ืกื™ื ืฉื‘ื•ืขื™ื™ื\n"
col_info += f" - ืขืจื›ื™ื ืืคืฉืจื™ื™ื: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday\n"
col_info += f" - ื“ื•ื’ืžืื•ืช: {', '.join(unique_days[:3])}\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE DayOfWeek = 'Monday', GROUP BY DayOfWeek, SELECT DayOfWeek, COUNT(*) GROUP BY DayOfWeek\n"
# Hour field
elif col == 'Hour':
hour_min = int(self.df[col].min())
hour_max = int(self.df[col].max())
col_info = f"โ€ข {col} (ืžืกืคืจ ืฉืœื 0-23): ืฉืขื” ื‘ื™ื•ื ืฉื‘ื” ื ื™ืชืŸ ื”ืžืฉื•ื‘\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžืืคืฉืจ ื ื™ืชื•ื— ืœืคื™ ืฉืขื•ืช ื”ื™ื•ื, ื–ื™ื”ื•ื™ ืฉืขื•ืช ืฉื™ื, ืชื›ื ื•ืŸ ื–ืžื™ื ื•ืช ืฉื™ืจื•ืช, ื•ื–ื™ื”ื•ื™ ื“ืคื•ืกื™ื ื™ื•ืžื™ื™ื\n"
col_info += f" - ื˜ื•ื•ื—: {hour_min} - {hour_max} (0=ื—ืฆื•ืช, 12=ืฆื”ืจื™ื™ื, 23=23:00)\n"
col_info += f" - ื“ื•ื’ืžืื•ืช: {', '.join([str(s) for s in samples[:3]])}\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE Hour >= 9 AND Hour <= 17 (ืฉืขื•ืช ืขื‘ื•ื“ื”), GROUP BY Hour, SELECT Hour, COUNT(*) GROUP BY Hour ORDER BY Hour\n"
# DayOrNight field
elif col == 'DayOrNight':
unique_values = get_unique_values(col, 5)
col_info = f"โ€ข {col} (ื˜ืงืกื˜): ื”ืื ื”ืžืฉื•ื‘ ื ื™ืชืŸ ื‘ืฉืขื•ืช ื”ื™ื•ื ืื• ื”ืœื™ืœื”\n"
col_info += f" - ืžืฉืžืขื•ืช ืขืกืงื™ืช: ืžืืคืฉืจ ื ื™ืชื•ื— ืœืคื™ ืฉืขื•ืช ืคืขื™ืœื•ืช, ื–ื™ื”ื•ื™ ื”ื‘ื“ืœื™ื ื‘ื™ืŸ ื™ื•ื ืœืœื™ืœื”, ืชื›ื ื•ืŸ ื–ืžื™ื ื•ืช ืฉื™ืจื•ืช\n"
col_info += f" - ืขืจื›ื™ื ืืคืฉืจื™ื™ื: 'ื™ื•ื' ืื• 'ืœื™ืœื”' (ื™ื•ื=6:00-18:00, ืœื™ืœื”=18:00-6:00)\n"
col_info += f" - ื“ื•ื’ืžืื•ืช: {', '.join(unique_values)}\n"
col_info += f" - ืฉื™ืžื•ืฉ ื‘ืฉืื™ืœืชื•ืช: WHERE DayOrNight = 'ื™ื•ื', GROUP BY DayOrNight, SELECT DayOrNight, COUNT(*) GROUP BY DayOrNight\n"
# Default for any other columns
else:
if dtype in ['int64', 'float64']:
val_min = self.df[col].min()
val_max = self.df[col].max()
col_info = f"โ€ข {col} ({dtype}): ืžืกืคืจ. ื˜ื•ื•ื—: {val_min} - {val_max}\n"
else:
unique_count = self.df[col].nunique()
col_info = f"โ€ข {col} ({dtype}): ื˜ืงืกื˜. {unique_count} ืขืจื›ื™ื ื™ื™ื—ื•ื“ื™ื™ื\n"
col_info += f" - ื“ื•ื’ืžืื•ืช: {', '.join(samples[:2])}\n"
all_columns_info += col_info + "\n"
# Build final schema info
total_records = len(self.df)
unique_services = self.df['ServiceName'].nunique() if 'ServiceName' in self.df.columns else 0
avg_level = self.df['Level'].mean() if 'Level' in self.df.columns else 0
schema_info = f"""ืฉื ื”ื˜ื‘ืœื”: Feedback_transformed (ืขื ืื•ืช ื’ื“ื•ืœื” F - ื—ื•ื‘ื”!)
ืกื˜ื˜ื™ืกื˜ื™ืงื•ืช ื›ืœืœื™ื•ืช:
- ืกืš ื”ื›ืœ ืžืฉื•ื‘ื™ื: {total_records}
- ืžืกืคืจ ืฉื™ืจื•ืชื™ื ื™ื™ื—ื•ื“ื™ื™ื: {unique_services}
- ื“ื™ืจื•ื’ ืžืžื•ืฆืข: {avg_level:.2f}
ืฉื“ื•ืช ื‘ื˜ื‘ืœื”:
{all_columns_info}
โš ๏ธ ื—ืฉื•ื‘: ื›ืœ ืฉืื™ืœืชื” ื—ื™ื™ื‘ืช ืœื”ืชื—ื™ืœ ื‘-SELECT ื•ืœื”ืฉืชืžืฉ ื‘-FROM Feedback_transformed!
"""
return schema_info
def analyze_query(self, query: str) -> AnalysisResult:
"""
Main analysis pipeline:
1. Analyze user query
2. Generate SQL queries
3. Execute SQL queries
4. Synthesize answer
"""
print(f"๐Ÿ” Analyzing query: {query}", flush=True)
if self.df is None:
error_msg = "No feedback data available. Please ensure feedback_transformed_2.csv exists in 0_preprocessing/ directory."
print(f"โŒ {error_msg}", flush=True)
raise ValueError(error_msg)
print(f"โœ… Data loaded: {len(self.df)} rows", flush=True)
# Check API keys
if not settings.gemini_api_key and not settings.openai_api_key:
error_msg = "โŒ No API keys configured! Please set GEMINI_API_KEY or OPENAI_API_KEY in Repository secrets."
print(error_msg, flush=True)
return AnalysisResult(
user_query=query,
sql_queries=[],
query_results=[],
summary=error_msg,
visualizations=None
)
print(f"โœ… API keys available: Gemini={bool(settings.gemini_api_key)}, OpenAI={bool(settings.openai_api_key)}", flush=True)
# Step 1: Generate SQL queries (with gibberish validation)
try:
print("๐Ÿ” Generating SQL queries...", flush=True)
sql_queries = self._generate_sql_queries(query)
print(f"โœ… Generated {len(sql_queries)} SQL queries", flush=True)
if len(sql_queries) == 0:
error_msg = "ืœื ื ื•ืฆืจื• ืฉืื™ืœืชื•ืช SQL. ื™ื™ืชื›ืŸ ืฉื”ืฉืืœื” ืœื ื‘ืจื•ืจื” ืื• ืฉื™ืฉ ื‘ืขื™ื” ืขื ื”-API. ื ืกื” ืœืฉืื•ืœ ืฉืืœื” ืื—ืจืช ืื• ื‘ื“ื•ืง ืืช ื”-API keys."
print(f"โŒ {error_msg}", flush=True)
return AnalysisResult(
user_query=query,
sql_queries=[],
query_results=[],
summary=error_msg,
visualizations=None
)
except ValueError as e:
# If query is gibberish, return a friendly error message
print(f"โŒ Query validation error: {e}", flush=True)
return AnalysisResult(
user_query=query,
sql_queries=[],
query_results=[],
summary=str(e),
visualizations=None
)
except Exception as e:
error_msg = f"ืฉื’ื™ืื” ื‘ื™ืฆื™ืจืช ืฉืื™ืœืชื•ืช SQL: {str(e)}. ื‘ื“ื•ืง ืืช ื”-API keys ื•ื”ื—ื™ื‘ื•ืจ ืœืื™ื ื˜ืจื ื˜."
print(f"โŒ {error_msg}", flush=True)
import traceback
traceback.print_exc()
return AnalysisResult(
user_query=query,
sql_queries=[],
query_results=[],
summary=error_msg,
visualizations=None
)
# Step 2: Execute SQL queries
print("๐Ÿ” Executing SQL queries...", flush=True)
query_results = self._execute_sql_queries(sql_queries)
successful_results = [r for r in query_results if not r.error and len(r.result) > 0]
print(f"โœ… Executed {len(query_results)} queries, {len(successful_results)} successful", flush=True)
# Step 3: Synthesize answer
print("๐Ÿ” Synthesizing answer...", flush=True)
summary = self._synthesize_answer(query, sql_queries, query_results)
# Step 4: (Optional) Generate visualizations
visualizations = self._generate_visualizations(query_results)
return AnalysisResult(
user_query=query,
sql_queries=sql_queries,
query_results=query_results,
summary=summary,
visualizations=visualizations
)
def _is_gibberish_query(self, query: str) -> bool:
"""
Check if the query is gibberish or unintelligible.
Returns True if the query appears to be gibberish, False otherwise.
"""
# Remove extra whitespace
query_clean = query.strip()
# Check if query is too short or empty
if len(query_clean) < 3:
return True
# Check if query contains only special characters or numbers
if not any(c.isalpha() for c in query_clean):
return True
# Check if query is mostly non-alphabetic characters
alpha_count = sum(1 for c in query_clean if c.isalpha())
if alpha_count < len(query_clean) * 0.3: # Less than 30% alphabetic
return True
return False
def _generate_sql_queries(self, query: str) -> List[str]:
"""
Use LLM to generate 1-5 SQL queries that will help answer the user's question.
"""
# Check if query is gibberish
if self._is_gibberish_query(query):
raise ValueError("ื”ืฉืืœื” ืœื ื‘ืจื•ืจื”. ืื ื ื ืกื— ืืช ื”ืฉืืœื” ื‘ืฆื•ืจื” ื™ื•ืชืจ ื‘ืจื•ืจื” ื•ืžืคื•ืจื˜ืช ื‘ืขื‘ืจื™ืช.")
schema_info = self._get_schema_info()
prompt = f"""ืฆื•ืจ ืฉืื™ืœืชื•ืช SQL ืœืฉืืœื”: {query}
{schema_info}
ื›ืœืœื™ื ื—ืฉื•ื‘ื™ื:
1. ืฉื ื”ื˜ื‘ืœื”: Feedback_transformed (ืขื ืื•ืช ื’ื“ื•ืœื” F) - ื—ื•ื‘ื” ื‘ื›ืœ ืฉืื™ืœืชื”!
2. ืฉืื™ืœืชื•ืช ื–ืžืŸ: ื”ืฉืชืžืฉ ื‘ืฉื“ื•ืช ื”ืžื—ื•ืฉื‘ื™ื (Year, Month, DayInMonth, DayOfWeek, Hour, DayOrNight) - ื”ื ื›ื‘ืจ ืžื•ื›ื ื™ื!
3. Level: 1-5 (1=ื’ืจื•ืข ืžืื•ื“, 5=ืžืขื•ืœื”) - ื”ืฉืชืžืฉ ื‘-Level ืœื ื™ืชื•ื— ืฉื‘ื™ืขื•ืช ืจืฆื•ืŸ
4. Text: ื—ื™ืคื•ืฉ ืขื LIKE '%ืžื™ืœื”%' - ืœื ื™ืชื•ื— ืชื•ื›ืŸ ื”ืžืฉื•ื‘ื™ื
5. ServiceName: ืฉื ื”ืฉื™ืจื•ืช - ืœื ื™ืชื•ื— ืœืคื™ ืฉื™ืจื•ืชื™ื
ื“ื•ื’ืžืื•ืช ืœืฉืื™ืœืชื•ืช:
- SELECT ServiceName, AVG(Level) as avg_rating, COUNT(*) as count FROM Feedback_transformed GROUP BY ServiceName ORDER BY avg_rating DESC
- SELECT COUNT(*) FROM Feedback_transformed WHERE Level >= 4
- SELECT Year, COUNT(*) as count FROM Feedback_transformed GROUP BY Year ORDER BY Year
- SELECT DayOfWeek, COUNT(*) as count FROM Feedback_transformed GROUP BY DayOfWeek
- SELECT ServiceName, Level, COUNT(*) as count FROM Feedback_transformed GROUP BY ServiceName, Level
ืคื•ืจืžื˜ ื”ืชืฉื•ื‘ื” - JSON ื‘ืœื‘ื“:
{{
"queries": [
"SELECT ... FROM Feedback_transformed ...",
"SELECT ... FROM Feedback_transformed ..."
]
}}
ืชืŸ ืจืง ืืช ื”-JSON, ืœืœื ื˜ืงืกื˜ ื ื•ืกืฃ."""
# Try Gemini first
if settings.gemini_api_key and genai is not None:
try:
print("๐Ÿ” Using Gemini API for SQL generation...", flush=True)
genai.configure(api_key=settings.gemini_api_key)
model = genai.GenerativeModel("gemini-2.0-flash")
response = model.generate_content(prompt)
text = getattr(response, "text", None)
if text:
print(f"โœ… Received response from Gemini: {text[:200]}...", flush=True)
queries = self._parse_sql_queries(text)
print(f"โœ… Parsed {len(queries)} SQL queries from Gemini response", flush=True)
return queries
else:
print("โŒ Gemini returned empty response", flush=True)
except Exception as e:
print(f"โŒ Gemini error in SQL generation: {e}", flush=True)
import traceback
traceback.print_exc()
# Fallback to OpenAI
if settings.openai_api_key and OpenAI is not None:
try:
print("๐Ÿ” Using OpenAI API for SQL generation...", flush=True)
client = OpenAI(api_key=settings.openai_api_key)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.3,
)
text = response.choices[0].message.content
if text:
print(f"โœ… Received response from OpenAI: {text[:200]}...", flush=True)
queries = self._parse_sql_queries(text)
print(f"โœ… Parsed {len(queries)} SQL queries from OpenAI response", flush=True)
return queries
else:
print("โŒ OpenAI returned empty response", flush=True)
except Exception as e:
print(f"โŒ OpenAI error in SQL generation: {e}", flush=True)
import traceback
traceback.print_exc()
# Fallback: return empty list
print("โŒ No API available or all APIs failed. Returning empty query list.", flush=True)
return []
def _parse_sql_queries(self, text: str) -> List[str]:
"""
Parse SQL queries from LLM response text.
The LLM is instructed to return JSON, but sometimes it may include
markdown formatting or return SQL directly. This function handles
multiple formats for robustness.
Args:
text: Raw text response from LLM (may be JSON, markdown, or plain SQL)
Returns:
List of SQL query strings, cleaned and validated.
Empty list if parsing fails completely.
Strategy:
1. First, try to parse as JSON (expected format)
2. If that fails, try to extract SQL queries using regex
3. Return empty list if both methods fail
"""
# Try to extract JSON first (expected format)
try:
# Remove markdown code blocks if present (LLM sometimes adds these)
text = re.sub(r'```json\s*', '', text)
text = re.sub(r'```\s*', '', text)
text = text.strip()
# Try to parse as JSON
data = json.loads(text)
if isinstance(data, dict) and "queries" in data:
queries = data["queries"]
if isinstance(queries, list):
# Filter out empty or invalid queries
return [q for q in queries if isinstance(q, str) and q.strip()]
except Exception:
# JSON parsing failed, try fallback method
pass
# Fallback: try to extract SQL queries directly using regex
# This handles cases where LLM returns SQL without JSON wrapper
sql_pattern = r'SELECT\s+.*?(?=\n\n|\nSELECT|$)'
matches = re.findall(sql_pattern, text, re.IGNORECASE | re.DOTALL)
if matches:
return [m.strip() for m in matches]
# If all parsing methods fail, return empty list
# The calling function will handle this gracefully
return []
def _execute_sql_queries(self, sql_queries: List[str]) -> List[SQLQueryResult]:
"""
Execute SQL queries on the feedback DataFrame using SQLite in-memory database.
This method creates a temporary SQLite database in memory, loads the
feedback DataFrame into it, and executes each SQL query. Errors are
caught per-query so one failing query doesn't stop the others.
Args:
sql_queries: List of SQL query strings to execute
Returns:
List of SQLQueryResult objects, one per query. Each result contains
either the query results (DataFrame) or an error message.
Implementation details:
- Uses SQLite in-memory database (':memory:') for fast execution
- DataFrame is loaded into table named 'Feedback_transformed'
- Each query is executed independently (errors don't cascade)
- Connection is always closed in finally block for safety
"""
if self.df is None:
return []
results = []
# Create in-memory SQLite database
# Using in-memory is fast and doesn't require disk I/O
conn = sqlite3.connect(':memory:')
try:
# Write DataFrame to SQLite table named 'Feedback_transformed'
# if_exists='replace' ensures clean state on each execution
self.df.to_sql('Feedback_transformed', conn, index=False, if_exists='replace')
# Execute each query independently
# This allows partial success - if one query fails, others can still succeed
for query in sql_queries:
try:
# Execute query and get results as DataFrame
result_df = pd.read_sql_query(query, conn)
results.append(SQLQueryResult(
query=query,
result=result_df,
error=None
))
except Exception as e:
# Store error but continue with other queries
results.append(SQLQueryResult(
query=query,
result=pd.DataFrame(), # Empty DataFrame on error
error=str(e)
))
finally:
# Always close connection, even if errors occur
conn.close()
return results
def _evaluate_answer_quality(self, query: str, answer: str, sql_queries: List[str] = None, query_results: List = None) -> tuple[float, str]:
"""
Evaluate the quality of an answer using an LLM reviewer.
Args:
query: The user's original question
answer: The synthesized answer to evaluate
sql_queries: List of SQL queries that were executed (optional, for context)
query_results: Results from executing those queries (optional, for context)
Returns:
tuple: (score 0-100, feedback/reasoning)
"""
# Build context about queries and results if available
context_text = ""
if sql_queries and query_results:
context_text = "\n\nื”ืฉืื™ืœืชื•ืช ืฉื‘ื•ืฆืขื•:\n"
for i, (q, r) in enumerate(zip(sql_queries, query_results), 1):
context_text += f"{i}. {q}\n"
if hasattr(r, 'error') and r.error:
context_text += f" ืฉื’ื™ืื”: {r.error}\n"
elif hasattr(r, 'result'):
context_text += f" ืชื•ืฆืื•ืช: {len(r.result) if hasattr(r.result, '__len__') else 'N/A'} ืฉื•ืจื•ืช\n"
evaluation_prompt = f"""ืืชื” ื‘ื•ื“ืง ืื™ื›ื•ืช ืชืฉื•ื‘ื•ืช ืžืงืฆื•ืขื™. ื”ืขืจืš ืืช ื”ืชืฉื•ื‘ื” ื”ื‘ืื”:
ืฉืืœืช ื”ืžืฉืชืžืฉ ื”ืžืงื•ืจื™ืช: {query}
{context_text}
ื”ืชืฉื•ื‘ื” ืฉื ื™ืชื ื”:
{answer}
โš ๏ธ ื”ืขืจืš ืืช ื”ืชืฉื•ื‘ื” ืœืคื™ ื”ืงืจื™ื˜ืจื™ื•ื ื™ื ื”ื‘ืื™ื (0-100):
1. ื”ืื ื”ืชืฉื•ื‘ื” ืขื•ื ื” ื™ืฉื™ืจื•ืช ืขืœ ื”ืฉืืœื” ื”ืžืงื•ืจื™ืช? (0-30 ื ืงื•ื“ื•ืช)
- ื”ืื ื”ืชืฉื•ื‘ื” ืžืชื™ื™ื—ืกืช ื™ืฉื™ืจื•ืช ืœืฉืืœื”: {query}?
- ืื ื”ืฉืืœื” ืžื‘ืงืฉืช ืกื™ื•ื•ื’/ื—ืœื•ืงื” ืœืคื™ ืฉื™ืจื•ืชื™ื (ServiceName) - ื”ืื ื”ืชืฉื•ื‘ื” ื›ื•ืœืœืช ื ื™ืชื•ื— ื ืคืจื“ ืœื›ืœ ืฉื™ืจื•ืช?
- ืื ื”ืฉืืœื” ืžื‘ืงืฉืช ืกื™ื•ื•ื’/ื—ืœื•ืงื” ืœืคื™ ื“ื™ืจื•ื’ื™ื (Level) - ื”ืื ื”ืชืฉื•ื‘ื” ื›ื•ืœืœืช ื ื™ืชื•ื— ื ืคืจื“ ืœื›ืœ ื“ื™ืจื•ื’?
- ืื ื”ืฉืืœื” ืžื‘ืงืฉืช ืกื™ื•ื•ื’/ื—ืœื•ืงื” ืœืคื™ ืชืืจื™ื›ื™ื - ื”ืื ื”ืชืฉื•ื‘ื” ื›ื•ืœืœืช ื ื™ืชื•ื— ื ืคืจื“ ืœืคื™ ืชืงื•ืคื•ืช?
- ื”ืื ื”ืชืฉื•ื‘ื” ื”ื™ื ืชืฉื•ื‘ื” ืžื™ืœื•ืœื™ืช ืžืคื•ืจื˜ืช ื•ืœื ืจืง ื”ื•ื“ืขื” ืฉืฉืื™ืœืชื•ืช ื‘ื•ืฆืขื•?
2. ื”ืื ื”ืชืฉื•ื‘ื” ืžื‘ื•ืกืกืช ืขืœ ื”ื ืชื•ื ื™ื ื•ื”ืฉืื™ืœืชื•ืช? (0-25 ื ืงื•ื“ื•ืช)
- ื”ืื ื”ืชืฉื•ื‘ื” ืžืฉืชืžืฉืช ื‘ื ืชื•ื ื™ื ืžื”ืฉืื™ืœืชื•ืช?
- ื”ืื ื”ืชืฉื•ื‘ื” ืžืกื‘ื™ืจื” ืื™ืš ื”ืฉืื™ืœืชื•ืช ืขื•ื–ืจื•ืช ืœืขื ื•ืช ืขืœ ื”ืฉืืœื”?
- ื”ืื ื”ืชืฉื•ื‘ื” ื›ื•ืœืœืช ืžืกืคืจื™ื ืžื“ื•ื™ืงื™ื ืžื”ืชื•ืฆืื•ืช?
3. ื”ืื ื”ืชืฉื•ื‘ื” ืžืคื•ืจื˜ืช ื•ืžืงื™ืคื”? (0-20 ื ืงื•ื“ื•ืช)
- ื”ืื ื”ืชืฉื•ื‘ื” ืืจื•ื›ื” ื•ืžืคื•ืจื˜ืช (ืœืคื—ื•ืช 400-600 ืžื™ืœื™ื)?
- ื”ืื ื”ืชืฉื•ื‘ื” ื›ื•ืœืœืช ื ื™ืชื•ื— ืžืขืžื™ืง ื•ืœื ืจืง ืจืฉื™ืžืช ื ืชื•ื ื™ื?
4. ื”ืื ื”ืชืฉื•ื‘ื” ื‘ืจื•ืจื” ื•ืงื•ื”ืจื ื˜ื™ืช? (0-15 ื ืงื•ื“ื•ืช)
- ื”ืื ื”ืชืฉื•ื‘ื” ื›ืชื•ื‘ื” ื‘ืฉืคื” ื‘ืจื•ืจื” ื•ืžื•ื‘ื ืช?
- ื”ืื ื”ืชืฉื•ื‘ื” ืžืื•ืจื’ื ืช ื”ื™ื˜ื‘ (ืœื ื’ื™ื‘ื•ื‘ ืฉืœ ืžื™ืœื™ื)?
5. ื”ืื ื”ืชืฉื•ื‘ื” ื›ื•ืœืœืช ืชื•ื‘ื ื•ืช ืขืกืงื™ื•ืช? (0-10 ื ืงื•ื“ื•ืช)
- ื”ืื ื”ืชืฉื•ื‘ื” ื›ื•ืœืœืช ืชื•ื‘ื ื•ืช ืขืœ ืชื”ืœื™ื›ื™ื ื“ื™ื’ื™ื˜ืœื™ื™ื?
- ื”ืื ื”ืชืฉื•ื‘ื” ื›ื•ืœืœืช ื”ืžืœืฆื•ืช ืžืขืฉื™ื•ืช?
ืชืŸ ืฆื™ื•ืŸ ื›ื•ืœืœ (0-100) ื•ื”ืกื‘ืจ ืงืฆืจ (2-3 ืžืฉืคื˜ื™ื) ืœืžื” ื”ืฆื™ื•ืŸ ื”ื–ื”.
ืคื•ืจืžื˜ ื”ืชืฉื•ื‘ื” - JSON ื‘ืœื‘ื“:
{{
"score": <ืžืกืคืจ 0-100>,
"reasoning": "<ื”ืกื‘ืจ ืงืฆืจ>"
}}
ืชืŸ ืจืง ืืช ื”-JSON, ืœืœื ื˜ืงืกื˜ ื ื•ืกืฃ."""
# Try Gemini first
if settings.gemini_api_key and genai is not None:
try:
genai.configure(api_key=settings.gemini_api_key)
model = genai.GenerativeModel("gemini-2.0-flash")
response = model.generate_content(evaluation_prompt)
text = getattr(response, "text", None)
if text:
# Try to parse JSON from response
# Extract JSON (may be wrapped in markdown or other text)
json_match = re.search(r'\{[^}]+\}', text, re.DOTALL)
if json_match:
try:
data = json.loads(json_match.group())
score = float(data.get('score', 0))
reasoning = data.get('reasoning', '')
return score, reasoning
except (json.JSONDecodeError, ValueError, KeyError):
pass
except Exception as e:
print(f"Gemini error in evaluation: {e}", flush=True)
# Fallback to OpenAI
if settings.openai_api_key and OpenAI is not None:
try:
client = OpenAI(api_key=settings.openai_api_key)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": evaluation_prompt}],
temperature=0.3,
)
text = response.choices[0].message.content
if text:
# Try to parse JSON from response
json_match = re.search(r'\{[^}]+\}', text, re.DOTALL)
if json_match:
try:
data = json.loads(json_match.group())
score = float(data.get('score', 0))
reasoning = data.get('reasoning', '')
return score, reasoning
except (json.JSONDecodeError, ValueError, KeyError):
pass
except Exception as e:
print(f"OpenAI error in evaluation: {e}", flush=True)
# Default: return high score if evaluation fails (don't block)
return 85.0, "ืœื ื ื™ืชืŸ ืœื”ืขืจื™ืš - ืžื—ื–ื™ืจ ืฆื™ื•ืŸ ื‘ืจื™ืจืช ืžื—ื“ืœ"
def _synthesize_answer(self, query: str, sql_queries: List[str],
query_results: List[SQLQueryResult], max_retries: int = 2) -> str:
"""
Use LLM to synthesize a comprehensive answer from:
- User query
- SQL queries that were executed
- Results of those queries
Includes quality evaluation and automatic improvement if score < 80.
Args:
query: The user's original question
sql_queries: List of SQL queries that were executed
query_results: Results from executing those queries
max_retries: Maximum number of retry attempts if quality is low
Returns:
Final synthesized answer
"""
# Format query results for the prompt
results_text = ""
for i, qr in enumerate(query_results, 1):
results_text += f"\nืฉืื™ืœืชื” {i}:\n{qr.query}\n\n"
if qr.error:
results_text += f"ืฉื’ื™ืื”: {qr.error}\n\n"
else:
# Format result as table
if len(qr.result) == 0:
results_text += "ืชื•ืฆืื”: ืื™ืŸ ืชื•ืฆืื•ืช\n\n"
else:
results_text += f"ืชื•ืฆืื” ({len(qr.result)} ืฉื•ืจื•ืช):\n"
results_text += qr.result.to_string(index=False)
results_text += "\n\n"
prompt = f"""ืืชื” ืื ืœื™ืกื˜ ื ืชื•ื ื™ื. ื”ืžืฉืชืžืฉ ืฉืืœ ืฉืืœื” ืขืœ ืžืฉื•ื‘ื™ ืžืฉืชืžืฉื™ื.
ืฉืืœืช ื”ืžืฉืชืžืฉ: {query}
ืชื•ืฆืื•ืช ื”ืฉืื™ืœืชื•ืช:
{results_text}
ื›ืชื•ื‘ ืชืฉื•ื‘ื” ืงืฆืจื”, ืžื“ื•ื™ืงืช ื•ืžืกื•ื“ืจืช ืฉืžื‘ื•ืกืกืช ื™ืฉื™ืจื•ืช ืขืœ ื”ืชื•ืฆืื•ืช.
โš ๏ธ ื›ืœืœื™ื:
1. ืขื ื” ื™ืฉื™ืจื•ืช ืขืœ ื”ืฉืืœื” - ืœื ื™ื•ืชืจ, ืœื ืคื—ื•ืช
2. ื”ืฉืชืžืฉ ื‘ืžืกืคืจื™ื ื”ืžื“ื•ื™ืงื™ื ืžื”ืชื•ืฆืื•ืช
3. ืคืกืงืื•ืช ืงืฆืจื•ืช (2-3 ืžืฉืคื˜ื™ื ื›ืœ ืื—ืช)
4. ืื ื™ืฉ ืกื™ื•ื•ื’ - ืจืฉื•ื ื›ืœ ืงื˜ื’ื•ืจื™ื” ืขื ื”ืžืกืคืจ ืฉืœื”
5. ืขื‘ืจื™ืช ืคืฉื•ื˜ื” ื•ื‘ืจื•ืจื”
ืžื‘ื ื”:
- ืคืกืงื” ืจืืฉื•ื ื”: ืชืฉื•ื‘ื” ื™ืฉื™ืจื” ืœืฉืืœื” ืขื ื”ืžืกืคืจื™ื ื”ืขื™ืงืจื™ื™ื
- ืคืกืงืื•ืช ื ื•ืกืคื•ืช: ืคื™ืจื•ื˜ ืœืคื™ ื”ืฆื•ืจืš (ืื ื™ืฉ ืกื™ื•ื•ื’ - ื›ืœ ืงื˜ื’ื•ืจื™ื” ื‘ื ืคืจื“)
- ืžืฉืคื˜ ืกื™ื›ื•ื: ื”ืžืกืงื ื” ื”ืขื™ืงืจื™ืช
โš ๏ธ ื—ืฉื•ื‘: ืชืฉื•ื‘ื” ืงืฆืจื” ื•ืžื“ื•ื™ืงืช. ืœื ืคื™ืœื•ืกื•ืคื™ื”, ืœื ื”ืกื‘ืจื™ื ืืจื•ื›ื™ื - ืจืง ื”ื ืชื•ื ื™ื ื•ื”ืžืกืงื ื•ืช.
ืื ื™ืฉ ืฉื’ื™ืื•ืช ื‘ืฉืื™ืœืชื•ืช, ืฆื™ื™ืŸ ื–ืืช ื‘ืงืฆืจื”."""
# Try Gemini first
if settings.gemini_api_key and genai is not None:
try:
genai.configure(api_key=settings.gemini_api_key)
model = genai.GenerativeModel("gemini-2.0-flash")
generation_config = {
"temperature": 0.8,
"top_p": 0.95,
"top_k": 40,
"max_output_tokens": 4000,
}
response = model.generate_content(prompt, generation_config=generation_config)
text = getattr(response, "text", None)
if text and text.strip():
answer = text.strip()
# Evaluate answer quality with context
score, reasoning = self._evaluate_answer_quality(query, answer, sql_queries, query_results)
print(f"Answer quality score: {score:.1f}/100 - {reasoning}", flush=True)
# If score is below 80, try to improve
if score < 80 and max_retries > 0:
print(f"Answer quality below threshold (80). Attempting improvement...", flush=True)
improvement_prompt = f"""ื”ืชืฉื•ื‘ื” ื”ืงื•ื“ืžืช ืงื™ื‘ืœื” ืฆื™ื•ืŸ {score}/100. ื”ืกื™ื‘ื”: {reasoning}
ืฉืืœืช ื”ืžืฉืชืžืฉ: {query}
ื”ืชืฉื•ื‘ื” ื”ืงื•ื“ืžืช (ืฉืฆืจื™ืš ืœืฉืคืจ):
{answer}
ืชื•ืฆืื•ืช ื”ืฉืื™ืœืชื•ืช:
{results_text}
ื›ืชื•ื‘ ืชืฉื•ื‘ื” ืžืฉื•ืคืจืช - ืงืฆืจื”, ืžื“ื•ื™ืงืช ื•ืžืกื•ื“ืจืช.
โš ๏ธ ื›ืœืœื™ื:
1. ืขื ื” ื™ืฉื™ืจื•ืช ืขืœ ื”ืฉืืœื” - ืœื ื™ื•ืชืจ, ืœื ืคื—ื•ืช
2. ื”ืฉืชืžืฉ ื‘ืžืกืคืจื™ื ื”ืžื“ื•ื™ืงื™ื ืžื”ืชื•ืฆืื•ืช
3. ืคืกืงืื•ืช ืงืฆืจื•ืช (2-3 ืžืฉืคื˜ื™ื ื›ืœ ืื—ืช)
4. ืื ื™ืฉ ืกื™ื•ื•ื’ - ืจืฉื•ื ื›ืœ ืงื˜ื’ื•ืจื™ื” ืขื ื”ืžืกืคืจ ืฉืœื”
5. ืขื‘ืจื™ืช ืคืฉื•ื˜ื” ื•ื‘ืจื•ืจื”
ืžื‘ื ื”:
- ืคืกืงื” ืจืืฉื•ื ื”: ืชืฉื•ื‘ื” ื™ืฉื™ืจื” ืœืฉืืœื” ืขื ื”ืžืกืคืจื™ื ื”ืขื™ืงืจื™ื™ื
- ืคืกืงืื•ืช ื ื•ืกืคื•ืช: ืคื™ืจื•ื˜ ืœืคื™ ื”ืฆื•ืจืš (ืื ื™ืฉ ืกื™ื•ื•ื’ - ื›ืœ ืงื˜ื’ื•ืจื™ื” ื‘ื ืคืจื“)
- ืžืฉืคื˜ ืกื™ื›ื•ื: ื”ืžืกืงื ื” ื”ืขื™ืงืจื™ืช
โš ๏ธ ื—ืฉื•ื‘: ืชืฉื•ื‘ื” ืงืฆืจื” ื•ืžื“ื•ื™ืงืช. ืœื ืคื™ืœื•ืกื•ืคื™ื”, ืœื ื”ืกื‘ืจื™ื ืืจื•ื›ื™ื - ืจืง ื”ื ืชื•ื ื™ื ื•ื”ืžืกืงื ื•ืช."""
try:
response = model.generate_content(improvement_prompt, generation_config=generation_config)
improved_text = getattr(response, "text", None)
if improved_text and improved_text.strip():
# Re-evaluate improved answer with context
improved_score, improved_reasoning = self._evaluate_answer_quality(query, improved_text.strip(), sql_queries, query_results)
print(f"Improved answer quality score: {improved_score:.1f}/100 - {improved_reasoning}", flush=True)
if improved_score > score:
return improved_text.strip()
except Exception as e:
print(f"Error improving answer: {e}", flush=True)
return answer
except Exception as e:
print(f"Gemini error in synthesis: {e}", flush=True)
# Fallback to OpenAI
if settings.openai_api_key and OpenAI is not None:
try:
client = OpenAI(api_key=settings.openai_api_key)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0.8,
max_tokens=3000,
)
text = response.choices[0].message.content
if text and text.strip():
answer = text.strip()
# Evaluate answer quality with context
score, reasoning = self._evaluate_answer_quality(query, answer, sql_queries, query_results)
print(f"Answer quality score: {score:.1f}/100 - {reasoning}", flush=True)
# If score is below 80, try to improve
if score < 80 and max_retries > 0:
print(f"Answer quality below threshold (80). Attempting improvement...", flush=True)
improvement_prompt = f"""ื”ืชืฉื•ื‘ื” ื”ืงื•ื“ืžืช ืงื™ื‘ืœื” ืฆื™ื•ืŸ {score}/100. ื”ืกื™ื‘ื”: {reasoning}
ืฉืืœืช ื”ืžืฉืชืžืฉ: {query}
ื”ืชืฉื•ื‘ื” ื”ืงื•ื“ืžืช (ืฉืฆืจื™ืš ืœืฉืคืจ):
{answer}
ืชื•ืฆืื•ืช ื”ืฉืื™ืœืชื•ืช:
{results_text}
ื›ืชื•ื‘ ืชืฉื•ื‘ื” ืžืฉื•ืคืจืช - ืงืฆืจื”, ืžื“ื•ื™ืงืช ื•ืžืกื•ื“ืจืช.
โš ๏ธ ื›ืœืœื™ื:
1. ืขื ื” ื™ืฉื™ืจื•ืช ืขืœ ื”ืฉืืœื” - ืœื ื™ื•ืชืจ, ืœื ืคื—ื•ืช
2. ื”ืฉืชืžืฉ ื‘ืžืกืคืจื™ื ื”ืžื“ื•ื™ืงื™ื ืžื”ืชื•ืฆืื•ืช
3. ืคืกืงืื•ืช ืงืฆืจื•ืช (2-3 ืžืฉืคื˜ื™ื ื›ืœ ืื—ืช)
4. ืื ื™ืฉ ืกื™ื•ื•ื’ - ืจืฉื•ื ื›ืœ ืงื˜ื’ื•ืจื™ื” ืขื ื”ืžืกืคืจ ืฉืœื”
5. ืขื‘ืจื™ืช ืคืฉื•ื˜ื” ื•ื‘ืจื•ืจื”
ืžื‘ื ื”:
- ืคืกืงื” ืจืืฉื•ื ื”: ืชืฉื•ื‘ื” ื™ืฉื™ืจื” ืœืฉืืœื” ืขื ื”ืžืกืคืจื™ื ื”ืขื™ืงืจื™ื™ื
- ืคืกืงืื•ืช ื ื•ืกืคื•ืช: ืคื™ืจื•ื˜ ืœืคื™ ื”ืฆื•ืจืš (ืื ื™ืฉ ืกื™ื•ื•ื’ - ื›ืœ ืงื˜ื’ื•ืจื™ื” ื‘ื ืคืจื“)
- ืžืฉืคื˜ ืกื™ื›ื•ื: ื”ืžืกืงื ื” ื”ืขื™ืงืจื™ืช
โš ๏ธ ื—ืฉื•ื‘: ืชืฉื•ื‘ื” ืงืฆืจื” ื•ืžื“ื•ื™ืงืช. ืœื ืคื™ืœื•ืกื•ืคื™ื”, ืœื ื”ืกื‘ืจื™ื ืืจื•ื›ื™ื - ืจืง ื”ื ืชื•ื ื™ื ื•ื”ืžืกืงื ื•ืช."""
try:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": improvement_prompt}],
temperature=0.8,
max_tokens=3000,
)
improved_text = response.choices[0].message.content
if improved_text and improved_text.strip():
# Re-evaluate improved answer with context
improved_score, improved_reasoning = self._evaluate_answer_quality(query, improved_text.strip(), sql_queries, query_results)
print(f"Improved answer quality score: {improved_score:.1f}/100 - {improved_reasoning}", flush=True)
if improved_score > score:
return improved_text.strip()
except Exception as e:
print(f"Error improving answer: {e}", flush=True)
return answer
except Exception as e:
print(f"OpenAI error in synthesis: {e}", flush=True)
# Fallback: generate a detailed answer from query results even if LLM failed
# This ensures we always return a meaningful answer, not just a status message
successful_results = [r for r in query_results if not r.error and len(r.result) > 0]
failed_results = [r for r in query_results if r.error]
if len(sql_queries) == 0:
return "ืœื ื ื•ืฆืจื• ืฉืื™ืœืชื•ืช SQL. ื™ื™ืชื›ืŸ ืฉื”ืฉืืœื” ืœื ื‘ืจื•ืจื” ืื• ืฉื™ืฉ ื‘ืขื™ื” ืขื ื”-API. ื ืกื” ืœืฉืื•ืœ ืฉืืœื” ืื—ืจืช ืื• ื‘ื“ื•ืง ืืช ื”-API keys ื‘-Repository secrets."
if successful_results:
fallback_text = f"ืกื™ื›ื•ื ืžืคื•ืจื˜ ืฉืœ ื”ืžืžืฆืื™ื:\n\n"
fallback_text += f"ื‘ื•ืฆืขื• {len(sql_queries)} ืฉืื™ืœืชื•ืช, ืžืชื•ื›ืŸ {len(successful_results)} ื”ืฆืœื™ื—ื• ื•ื”ื—ื–ื™ืจื• ืชื•ืฆืื•ืช.\n"
if failed_results:
fallback_text += f"โš ๏ธ {len(failed_results)} ืฉืื™ืœืชื•ืช ื ื›ืฉืœื•.\n"
fallback_text += "\n"
# Analyze and summarize each result
for i, qr in enumerate(successful_results, 1):
fallback_text += f"ืžืžืฆืื™ื ืžืฉืื™ืœืชื” {i}:\n"
fallback_text += f"ืฉืื™ืœืชื”: {qr.query}\n"
fallback_text += f"ืžืกืคืจ ืจืฉื•ืžื•ืช: {len(qr.result)}\n\n"
# Try to provide meaningful analysis
if len(qr.result) > 0:
fallback_text += "ืชื•ืฆืื•ืช:\n"
# Show summary statistics if possible
numeric_cols = qr.result.select_dtypes(include=['number']).columns
if len(numeric_cols) > 0:
fallback_text += "ืกื˜ื˜ื™ืกื˜ื™ืงื•ืช:\n"
for col in numeric_cols[:3]: # Limit to first 3 numeric columns
fallback_text += f"- {col}: ืžืžื•ืฆืข {qr.result[col].mean():.2f}, ืกื›ื•ื {qr.result[col].sum():.0f}\n"
fallback_text += "\n"
# Show sample data
fallback_text += "ื“ื•ื’ืžืื•ืช ืžื”ื ืชื•ื ื™ื:\n"
fallback_text += qr.result.head(5).to_string(index=False)
fallback_text += "\n\n"
if failed_results:
fallback_text += "\nืฉื’ื™ืื•ืช ื‘ืฉืื™ืœืชื•ืช:\n"
for i, qr in enumerate(failed_results, 1):
fallback_text += f"ืฉืื™ืœืชื” {i}: {qr.error}\n"
fallback_text += "\n"
fallback_text += "ื”ืขืจื”: ืชืฉื•ื‘ื” ื–ื• ื ื•ืฆืจื” ืื•ื˜ื•ืžื˜ื™ืช ืžื”ืชื•ืฆืื•ืช. ืœื ื™ืชื•ื— ืžืคื•ืจื˜ ื™ื•ืชืจ, ื ืกื” ืœืฉืื•ืœ ืฉืืœื” ืกืคืฆื™ืคื™ืช ื™ื•ืชืจ."
return fallback_text
else:
# If no successful results, provide detailed error information
error_details = ""
if failed_results:
error_details = "\n\nืฉื’ื™ืื•ืช ื‘ืฉืื™ืœืชื•ืช:\n"
for i, qr in enumerate(failed_results, 1):
error_details += f"ืฉืื™ืœืชื” {i}: {qr.query}\n"
error_details += f"ืฉื’ื™ืื”: {qr.error}\n\n"
return f"ื‘ื•ืฆืขื• {len(sql_queries)} ืฉืื™ืœืชื•ืช, ืืš ืœื ื”ืชืงื‘ืœื• ืชื•ืฆืื•ืช ืžื”ื ืชื•ื ื™ื.{error_details}\nื™ื™ืชื›ืŸ ืฉื”ื ืชื•ื ื™ื ืœื ืžื›ื™ืœื™ื ืžื™ื“ืข ื”ืชื•ืื ืœืฉืืœื” ืฉื ืฉืืœื”. ื ืกื” ืœืฉืื•ืœ ืฉืืœื” ืื—ืจืช ืื• ืœื‘ื“ื•ืง ืืช ื”ื ืชื•ื ื™ื ื”ื–ืžื™ื ื™ื."
def _generate_visualizations(self, query_results: List[SQLQueryResult]) -> Optional[List[Dict[str, Any]]]:
"""
Generate visualization specifications for query results.
This function analyzes the structure of query results and automatically
determines the best visualization type (bar, line, scatter, histogram).
The specifications are returned as dictionaries that the frontend can
use with Chart.js to render the visualizations.
Args:
query_results: List of SQL query results to visualize
Returns:
List of visualization specification dictionaries, or None if no
visualizations can be generated. Each dict contains:
- type: Chart type (bar, line, scatter, histogram)
- title: Display title
- x, y: Column names for axes
- data: The actual data to visualize
Visualization selection logic:
- 2 columns: bar chart (categorical + numeric) or line chart (time series)
- 1 column: histogram (if numeric)
- 3+ columns: bar chart (first categorical + first numeric)
"""
visualizations = []
for i, qr in enumerate(query_results, 1):
# Skip queries that failed or returned no results
if qr.error or len(qr.result) == 0:
continue
# Determine visualization type based on result structure
result = qr.result
# If result has 2 columns, might be a bar chart or line chart
if len(result.columns) == 2:
col1, col2 = result.columns
# If first column is categorical and second is numeric
if result[col2].dtype in ['int64', 'float64']:
# Check if it's a time series (col1 looks like date/time)
if 'date' in col1.lower() or 'time' in col1.lower() or 'ืชืืจื™ืš' in col1.lower():
visualizations.append({
"type": "line",
"title": f"ืชื•ืฆืื” ืฉืœ ืฉืื™ืœืชื” {i}",
"x": col1,
"y": col2,
"x_label": col1,
"y_label": col2,
"data": result.to_dict('records')
})
else:
# Bar chart for categorical data
visualizations.append({
"type": "bar",
"title": f"ืชื•ืฆืื” ืฉืœ ืฉืื™ืœืชื” {i}",
"x": col1,
"y": col2,
"x_label": col1,
"y_label": col2,
"data": result.to_dict('records')
})
# If both are numeric, might be a scatter plot
elif result[col1].dtype in ['int64', 'float64'] and result[col2].dtype in ['int64', 'float64']:
visualizations.append({
"type": "scatter",
"title": f"ืชื•ืฆืื” ืฉืœ ืฉืื™ืœืชื” {i}",
"x": col1,
"y": col2,
"x_label": col1,
"y_label": col2,
"data": result.to_dict('records')
})
# If result has 1 column with numeric values, might be a distribution
elif len(result.columns) == 1:
col = result.columns[0]
if result[col].dtype in ['int64', 'float64']:
visualizations.append({
"type": "histogram",
"title": f"ืชื•ืฆืื” ืฉืœ ืฉืื™ืœืชื” {i}",
"x": col,
"x_label": col,
"data": result[col].tolist()
})
# If result has 3+ columns, try to find the best visualization
elif len(result.columns) >= 3:
# Look for numeric columns
numeric_cols = [c for c in result.columns if result[c].dtype in ['int64', 'float64']]
categorical_cols = [c for c in result.columns if result[c].dtype == 'object']
# If we have one categorical and one numeric, use bar chart
if len(categorical_cols) >= 1 and len(numeric_cols) >= 1:
cat_col = categorical_cols[0]
num_col = numeric_cols[0]
visualizations.append({
"type": "bar",
"title": f"ืชื•ืฆืื” ืฉืœ ืฉืื™ืœืชื” {i}",
"x": cat_col,
"y": num_col,
"x_label": cat_col,
"y_label": num_col,
"data": result.to_dict('records')
})
return visualizations if visualizations else None