# Required imports import torch torch.classes.__path__ = [] import os import uuid import random import time import tempfile import streamlit as st import pandas as pd import plotly.express as px import plotly.graph_objects as go from sqlalchemy import create_engine, text # Import necessary components from llama_index from llama_index.core import Settings from llama_index.llms.openrouter import OpenRouter from llama_index.embeddings.huggingface import HuggingFaceEmbedding # Import custom tools and workflow from tools import setup_document_tool, setup_sql_tool, get_codex_project_info from workflow import RouterOutputAgentWorkflow from llama_index.llms.openai import OpenAI # Apply nest_asyncio to allow running asyncio in Streamlit import asyncio import nest_asyncio nest_asyncio.apply() # Set page configuration st.set_page_config( page_title="Text2SQL + RAG hybrid query engine ⚙️ ", page_icon="🏙️", layout="wide", ) # Initialize session state if "file_uploaded" not in st.session_state: st.session_state.file_uploaded = False if "messages" not in st.session_state: st.session_state.messages = [] if "id" not in st.session_state: st.session_state.id = uuid.uuid4() st.session_state.file_cache = {} if "workflow" not in st.session_state: st.session_state.workflow = None if "workflow_needs_update" not in st.session_state: st.session_state.workflow_needs_update = False if "llm_initialized" not in st.session_state: st.session_state.llm_initialized = False if "codex_api_key" not in st.session_state: st.session_state.codex_api_key = "" if "openrouter_api_key" not in st.session_state: st.session_state.openrouter_api_key = "" ##################################### # Helper Functions ##################################### def reset_chat(): """Reset the chat history and clear context""" # Clear messages immediately if "messages" in st.session_state: st.session_state.messages = [] if "workflow" in st.session_state and st.session_state.workflow: st.session_state.workflow = None st.session_state.workflow_needs_update = True ##################################### # Database Visualization Functions ##################################### def get_database_connection(): """Create a database connection""" try: db_path = "city_database.sqlite" if not os.path.exists(db_path): st.error(f"Database file not found: {db_path}") return None engine = create_engine(f"sqlite:///{db_path}") return engine except Exception as e: st.error(f"Error connecting to database: {str(e)}") return None def get_table_data(table_name="city_stats"): """Get data from the specified table""" engine = get_database_connection() if engine is None: return None try: query = f"SELECT * FROM {table_name}" df = pd.read_sql_query(query, engine) return df except Exception as e: st.error(f"Error fetching data: {str(e)}") return None def get_table_schema(table_name="city_stats"): """Get the schema of the specified table""" engine = get_database_connection() if engine is None: return None try: with engine.connect() as conn: result = conn.execute(text(f"PRAGMA table_info({table_name})")) schema_info = result.fetchall() return schema_info except Exception as e: st.error(f"Error fetching schema: {str(e)}") return None def create_population_chart(df): """Create a population chart using Plotly""" if df is None or df.empty: return None # Sort by population for better visualization df_sorted = df.sort_values('population', ascending=False).head(10) fig = px.bar( df_sorted, x='city_name', y='population', title='Top 10 Cities by Population', labels={'city_name': 'City', 'population': 'Population'}, color='population', color_continuous_scale='viridis' ) fig.update_layout( xaxis_tickangle=-45, height=500, showlegend=False ) return fig def create_state_distribution_chart(df): """Create a state distribution chart""" if df is None or df.empty: return None # Count cities per state state_counts = df['state'].value_counts().head(10) fig = px.pie( values=state_counts.values, names=state_counts.index, title='Top 10 States by Number of Cities', hole=0.3 ) fig.update_layout(height=500) return fig def create_population_scatter(df): """Create a scatter plot of cities by population""" if df is None or df.empty: return None fig = px.scatter( df, x='city_name', y='population', size='population', color='state', title='City Population Distribution', labels={'city_name': 'City', 'population': 'Population', 'state': 'State'}, hover_data=['state'] ) fig.update_layout( xaxis_tickangle=-45, height=500 ) return fig def render_database_tab(): """Render the database visualization tab""" st.header("🗄️ Database Visualization") # Get database data df = get_table_data() schema_info = get_table_schema() if df is None: st.error("Unable to load database data. Please check if the database file exists.") return # Display database info with enhanced metrics col1, col2, col3, col4 = st.columns(4) with col1: st.metric("Total Cities", len(df)) with col2: st.metric("Total Population", f"{df['population'].sum():,}") with col3: st.metric("States", df['state'].nunique()) with col4: avg_population = int(df['population'].mean()) st.metric("Avg Population", f"{avg_population:,}") # Quick stats st.subheader("📈 Quick Statistics") stats_col1, stats_col2 = st.columns(2) with stats_col1: st.write("**Largest Cities:**") top_cities = df.nlargest(5, 'population')[['city_name', 'population', 'state']] for _, row in top_cities.iterrows(): st.write(f"• {row['city_name']}, {row['state']}: {row['population']:,}") with stats_col2: st.write("**States with Most Cities:**") state_counts = df['state'].value_counts().head(5) for state, count in state_counts.items(): st.write(f"• {state}: {count} cities") # Database Schema Section st.subheader("📋 Database Schema") if schema_info: schema_df = pd.DataFrame(schema_info, columns=['cid', 'name', 'type', 'notnull', 'dflt_value', 'pk']) st.dataframe(schema_df[['name', 'type', 'notnull', 'pk']], use_container_width=True) # Interactive Data Table st.subheader("📊 Interactive Data Table") st.write("Use the table below to explore the city data. You can sort, filter, and search through the data.") # Add search functionality search_term = st.text_input("🔍 Search cities:", placeholder="Enter city name or state...") # Filter data based on search if search_term: filtered_df = df[ df['city_name'].str.contains(search_term, case=False, na=False) | df['state'].str.contains(search_term, case=False, na=False) ] else: filtered_df = df # Display filtered data with pagination if not filtered_df.empty: st.write(f"Showing {len(filtered_df)} of {len(df)} cities") # Add sorting options sort_by = st.selectbox("Sort by:", ["city_name", "population", "state"]) sort_order = st.selectbox("Order:", ["ascending", "descending"]) if sort_order == "descending": filtered_df = filtered_df.sort_values(sort_by, ascending=False) else: filtered_df = filtered_df.sort_values(sort_by, ascending=True) # Display the dataframe with enhanced styling st.dataframe( filtered_df, use_container_width=True, height=400, column_config={ "city_name": st.column_config.TextColumn("City Name", width="medium"), "population": st.column_config.NumberColumn("Population", format=","), "state": st.column_config.TextColumn("State", width="medium") } ) else: st.warning("No cities found matching your search criteria.") # Charts Section st.subheader("📈 Data Visualizations") # Create tabs for different charts chart_tab1, chart_tab2, chart_tab3 = st.tabs(["Population Chart", "State Distribution", "Population Scatter"]) with chart_tab1: fig1 = create_population_chart(df) if fig1: st.plotly_chart(fig1, use_container_width=True) else: st.warning("Unable to create population chart.") with chart_tab2: fig2 = create_state_distribution_chart(df) if fig2: st.plotly_chart(fig2, use_container_width=True) else: st.warning("Unable to create state distribution chart.") with chart_tab3: fig3 = create_population_scatter(df) if fig3: st.plotly_chart(fig3, use_container_width=True) else: st.warning("Unable to create population scatter plot.") # Custom Query Section st.subheader("🔍 Custom SQL Queries") st.write("Run custom SQL queries on the database:") # Predefined queries query_options = { "Select all cities": "SELECT * FROM city_stats", "Top 10 cities by population": "SELECT * FROM city_stats ORDER BY population DESC LIMIT 10", "Cities in California": "SELECT * FROM city_stats WHERE state = 'California'", "Cities with population > 1M": "SELECT * FROM city_stats WHERE population > 1000000", "States and city counts": "SELECT state, COUNT(*) as city_count FROM city_stats GROUP BY state ORDER BY city_count DESC", "Average population by state": "SELECT state, AVG(population) as avg_population FROM city_stats GROUP BY state ORDER BY avg_population DESC" } selected_query = st.selectbox("Choose a predefined query:", list(query_options.keys())) custom_query = st.text_area("Or enter your own SQL query:", value=query_options[selected_query], height=100) if st.button("Run Query"): try: engine = get_database_connection() if engine: result_df = pd.read_sql_query(custom_query, engine) st.success(f"Query executed successfully! Found {len(result_df)} rows.") st.dataframe(result_df, use_container_width=True) # Download results csv = result_df.to_csv(index=False) st.download_button( label="Download Query Results", data=csv, file_name="query_results.csv", mime="text/csv" ) except Exception as e: st.error(f"Error executing query: {str(e)}") # Raw Data Section st.subheader("📄 Raw Data") st.write("Download the complete dataset:") # Add download button csv = df.to_csv(index=False) st.download_button( label="Download CSV", data=csv, file_name="city_stats.csv", mime="text/csv" ) # Display raw data st.dataframe(df, use_container_width=True) @st.cache_resource def initialize_model(_api_key): """Initialize models for LLM and embedding""" try: # Initialize models for LLM and embedding with OpenRouter # llm = OpenAI(model="gpt-4o-mini", api_key=_api_key) llm = OpenRouter(model="qwen/qwen-turbo", api_key=_api_key) embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5") return llm, embed_model except Exception as e: st.error(f"Error initializing models: {str(e)}") return None def handle_file_upload(uploaded_files): """Function to handle multiple file uploads with temporary directory""" try: # Create a temporary directory if it doesn't exist yet if not hasattr(st.session_state, "temp_dir") or not os.path.exists( st.session_state.temp_dir ): st.session_state.temp_dir = tempfile.mkdtemp() # Track uploaded files if "uploaded_files" not in st.session_state: st.session_state.uploaded_files = [] # Process each uploaded file file_paths = [] for uploaded_file in uploaded_files: # Save file to temporary location temp_file_path = os.path.join(st.session_state.temp_dir, uploaded_file.name) # Write the file with open(temp_file_path, "wb") as f: f.write(uploaded_file.getbuffer()) # Add to list of uploaded files st.session_state.uploaded_files.append( {"name": uploaded_file.name, "path": temp_file_path} ) file_paths.append(temp_file_path) st.session_state.file_uploaded = True st.session_state.current_pdf = ( file_paths[0] if file_paths else None ) # Set first file as current for preview st.session_state.workflow_needs_update = True # Mark workflow for update return file_paths except Exception as e: st.error(f"Error uploading files: {str(e)}") return None def initialize_workflow(tools): """Initialize workflow with the given tools""" try: workflow = RouterOutputAgentWorkflow(tools=tools, verbose=False, timeout=120) st.session_state.workflow = workflow return workflow except Exception as e: st.error(f"Error initializing workflow: {str(e)}") return None async def process_query(query, workflow): """Function to process a query using the workflow""" try: # Clear chat history before processing new query to avoid persistence workflow.chat_history = [] with st.spinner("Processing your query..."): # Add timeout to prevent hanging result = await asyncio.wait_for(workflow.run(message=query), timeout=60.0) # Extract tool usage information from chat history tool_usage_info = [] for msg in workflow.chat_history: if msg.role == "tool" and hasattr(msg, 'additional_kwargs'): tool_name = msg.additional_kwargs.get('tool_used', 'Unknown Tool') trust_score = msg.additional_kwargs.get('trust_score') # Skip cancelled or error messages in the display if "cancelled" not in msg.content.lower() and "error executing tool" not in msg.content.lower(): # Handle the response content - it might be a dictionary string response_content = msg.content # If it's a dictionary string (like the context not found case), extract the response if response_content.startswith("{'response':") or response_content.startswith('{"response":'): try: import ast # Try to safely evaluate the string as a dictionary response_dict = ast.literal_eval(response_content) if isinstance(response_dict, dict) and 'response' in response_dict: response_content = response_dict['response'] # Update trust_score if it's in the dict if 'trust_score' in response_dict and trust_score is None: trust_score = response_dict['trust_score'] except (ValueError, SyntaxError, TypeError) as e: # If parsing fails, keep the original content if st.session_state.get('verbose', False): print(f"Failed to parse response dict: {e}") pass tool_info = { 'tool_name': tool_name, 'response': response_content, 'trust_score': trust_score } tool_usage_info.append(tool_info) # Simple formatting logic: if we have tool responses, format them if tool_usage_info: formatted_response = "" for i, tool_info in enumerate(tool_usage_info, 1): # Determine if this is a document tool (anything not SQL) is_doc_tool = tool_info['tool_name'] != 'sql_tool' if is_doc_tool: formatted_response += "**🔧 Tool Used:** `document_tool`\n\n" else: formatted_response += f"**🔧 Tool Used:** `{tool_info['tool_name']}`\n\n" formatted_response += f"**📝 Response:**\n\n{tool_info['response']}\n\n" # Only show trust score for document tools if is_doc_tool and tool_info['trust_score'] is not None: trust_percentage = round(tool_info['trust_score'] * 100, 1) trust_emoji = "🟢" if trust_percentage >= 70 else "🟡" if trust_percentage >= 50 else "🔴" formatted_response += f"**{trust_emoji} Trust Score:** {trust_percentage}%\n\n" if i < len(tool_usage_info): formatted_response += "---\n\n" return formatted_response else: return result except asyncio.TimeoutError: print("Query processing timed out") return "The query took too long to process. Please try a simpler question or try again." except asyncio.CancelledError: print("Query processing was cancelled") return "The query processing was cancelled. Please try again." except Exception as e: print(f"Error in process_query: {str(e)}") return "I'm sorry, I encountered an error while processing your request. Please try a different question." ##################################### # Main Streamlit app ##################################### def main(): # Sidebar configuration with st.sidebar: st.title("Configuration 🔑") # Codex API Key input codex_logo_html = """