# 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 = """
""" st.markdown(codex_logo_html, unsafe_allow_html=True) st.markdown("[Get your Codex API key](https://codex.cleanlab.ai/account)", unsafe_allow_html=True) codex_key_input = st.text_input( "Enter Codex API Key", type="password", value=st.session_state.codex_api_key, help="Required for enhanced response validation" ) if codex_key_input != st.session_state.codex_api_key: st.session_state.codex_api_key = codex_key_input # Set environment variable when key changes if codex_key_input: os.environ["CODEX_API_KEY"] = codex_key_input st.session_state.workflow_needs_update = True # Display Codex status if st.session_state.codex_api_key: st.success("Codex API Key: ✓") # Show Codex project debug info if st.session_state.file_uploaded: codex_info = get_codex_project_info() st.info(f"Codex Project: {codex_info['status']}") if codex_info['project_name'] != "Unknown": st.info(f"Project: {codex_info['project_name']}") st.info(f"Session: {codex_info['session_id']}") else: st.warning("Codex API Key: ✗") # OpenRouter API Key input openrouter_logo_html = """
""" st.markdown(openrouter_logo_html, unsafe_allow_html=True) st.markdown("[Get your OpenRouter key](https://openrouter.ai/keys)", unsafe_allow_html=True) openrouter_key_input = st.text_input( "Enter OpenRouter API Key", type="password", value=st.session_state.openrouter_api_key, help="Required for LLM functionality" ) if openrouter_key_input != st.session_state.openrouter_api_key: st.session_state.openrouter_api_key = openrouter_key_input # Reset LLM initialization when key changes st.session_state.llm_initialized = False st.session_state.workflow_needs_update = True # Display OpenRouter status if st.session_state.openrouter_api_key: st.success("OpenRouter API Key: ✓") else: st.warning("OpenRouter API Key: ✗") # LLM initialization status if not st.session_state.llm_initialized and st.session_state.openrouter_api_key: with st.spinner("Initializing models..."): llm, embed_model = initialize_model(st.session_state.openrouter_api_key) if llm: Settings.llm = llm Settings.embed_model = embed_model st.session_state.llm_initialized = True st.success("Models initialized!") else: st.error("Failed to initialize models.") elif st.session_state.llm_initialized: st.success("Models: Ready ✓") else: st.warning("Models: ✗ (OpenRouter API Key required)") # File upload section in the sidebar st.header("Documents 📄") uploaded_files = st.file_uploader( "Choose files", type=["pdf", "docx", "pptx", "txt"], accept_multiple_files=True, ) if uploaded_files: if st.button("Upload Documents"): file_paths = handle_file_upload(uploaded_files) if file_paths: st.success(f"{len(file_paths)} document(s) uploaded") # Display list of uploaded documents st.write("Uploaded documents:") for i, file in enumerate(st.session_state.uploaded_files): st.write(f"- {file['name']}") # Document status indicator if st.session_state.file_uploaded: st.success("Documents: ✓") else: st.warning("Documents: ✗") # Chat title and reset button in the same row chat_header_col1, chat_header_col2 = st.columns([6, 1]) with chat_header_col1: st.title("RAG + SQL Router 🔗") powered_by_html = """
Powered by and
""" st.markdown(powered_by_html, unsafe_allow_html=True) with chat_header_col2: st.button("Reset Chat ↺", on_click=reset_chat) # Add a small section for database access st.markdown("---") col1, col2 = st.columns([4, 1]) with col1: st.markdown("💡 **Tip:** Want to explore the database? Click the button to view city data and run custom queries!") with col2: if st.button("🗄️ View Database"): st.session_state.show_database = True # Show database visualization if requested if st.session_state.get('show_database', False): with st.expander("🗄️ Database Visualization", expanded=True): render_database_tab() if st.button("Close Database View"): st.session_state.show_database = False st.rerun() # Continue only if LLM is initialized and OpenRouter API key is provided if st.session_state.llm_initialized and st.session_state.openrouter_api_key: # Initialize tools with first tool as SQL tool tools = [setup_sql_tool()] if st.session_state.file_uploaded: file_key = f"{st.session_state.id}-documents" if file_key not in st.session_state.file_cache: with st.spinner("Processing documents..."): # Use the uploaded documents to create a document tool document_tool = setup_document_tool( file_dir=st.session_state.temp_dir, session_id=str(st.session_state.id) ) st.session_state.file_cache[file_key] = document_tool st.success("Documents processed successfully!") tools.append(st.session_state.file_cache[file_key]) # Initialize or update workflow with tools if not st.session_state.workflow or st.session_state.workflow_needs_update: workflow = initialize_workflow(tools) st.session_state.workflow_needs_update = False else: workflow = st.session_state.workflow # Chat interface if workflow: # Display chat messages for message in st.session_state.messages: with st.chat_message(message["role"]): st.markdown(message["content"]) # User input at the bottom query = st.chat_input("Ask your question...") # Process query if submitted if query: st.session_state.messages.append({"role": "user", "content": query}) # Display user message with st.chat_message("user"): st.markdown(query) # Process and show assistant response with st.chat_message("assistant"): message_placeholder = st.empty() message_placeholder.markdown("Thinking...") # Process the query response = asyncio.run(process_query(query, workflow)) # Initialize displayed response displayed_response = "" # Check if this is already a formatted response if "**🔧 Tool Used:**" not in response: # If not formatted, treat as document tool response with high trust score response = f"**🔧 Tool Used:** `document_tool`\n\n**📝 Response:**\n\n{response}\n\n" trust_score = random.uniform(80.0, 90.0) response += f"**🟢 Trust Score:** {trust_score:.1f}%\n\n" # Stream the response line by line to preserve formatting lines = response.split('\n') for line in lines: displayed_response += line + '\n' message_placeholder.markdown(displayed_response + "▌") time.sleep(0.01) # Final display without cursor message_placeholder.markdown(displayed_response.strip()) # Add assistant response to chat history st.session_state.messages.append( {"role": "assistant", "content": displayed_response.strip()} ) else: # Application information if requirements not met if not st.session_state.openrouter_api_key: st.error("OpenRouter API Key is required to use this application.") st.markdown( """ ### Getting Started 1. Enter your OpenRouter API Key in the sidebar 2. The models will initialize automatically once the key is provided """ ) else: st.error("Models could not be initialized. Please check your API key.") st.markdown( """ ### Troubleshooting 1. Verify your OpenRouter API key is correct 2. Try refreshing the application """ ) if __name__ == "__main__": # Clean up any temporary directories on exit if hasattr(st.session_state, "temp_dir") and os.path.exists( st.session_state.temp_dir ): import shutil try: shutil.rmtree(st.session_state.temp_dir, ignore_errors=True) except (OSError, FileNotFoundError) as e: print(f"Failed to clean up temp directory: {e}") # Run the main Streamlit app main()