Data-Flow / streamlit_app.py
transformer03's picture
added the toggle for fallback
5875a4c
Raw
History Blame Contribute Delete
28.7 kB
# streamlit_app.py
import streamlit as st
import os
import sys
import shutil
import pandas as pd
import subprocess
import time
import uuid
import io
import json
import re
from pathlib import Path
from datetime import datetime
# --- Local Project Imports ---
from config import (
CRAWL_CACHE_DIR, KNOWLEDGE_CACHE_DIR, OUTPUT_DIR, VECTOR_DB_PATH, APP_VERSION,
SUPABASE_URL, SUPABASE_KEY, APP_PASSWORD
)
from schemas import CHOICE_OPTIONS
import db_manager
# --- Check Configuration Status ---
try:
from config import MISTRAL_API_KEY
SECRETS_LOADED = bool(MISTRAL_API_KEY)
from config import SUPABASE_CONFIGURED
except ImportError:
SECRETS_LOADED = False
SUPABASE_CONFIGURED = False
# --- App Directories ---
TEMP_DIR = Path("./temp_streamlit_files")
TEMP_DIR.mkdir(exist_ok=True)
LOG_DIR = Path("./streamlit_logs")
LOG_DIR.mkdir(exist_ok=True)
# --- Initial Page Config ---
st.set_page_config(page_title="Crawl4AI Unified System", layout="wide", initial_sidebar_state="auto")
# ==========================================
# --- SECURITY / AUTHENTICATION LAYER ---
# ==========================================
def check_password():
"""Returns `True` if the user had the correct password."""
if not APP_PASSWORD:
st.error("⚠️ **CRITICAL ERROR:** 'APP_PASSWORD' secret is not set in Hugging Face Secrets. Access disabled.")
return False
if st.session_state.get("password_correct", False):
return True
# Show input for password.
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
st.write("")
st.write("")
with st.container(border=True):
st.title("🔒 Restricted Access")
st.markdown("Please enter the application password to continue.")
pwd_input = st.text_input("Password", type="password", key="password_input")
if st.button("Login", type="primary", use_container_width=True):
if pwd_input == APP_PASSWORD:
st.session_state["password_correct"] = True
st.rerun()
else:
st.error("😕 Password incorrect")
return False
# Block execution until authenticated
if not check_password():
st.stop()
# ==========================================
# --- MAIN APPLICATION LOGIC ---
# ==========================================
# --- App State Management ---
def initialize_session_state():
"""Initializes all necessary session state variables."""
if 'view' not in st.session_state:
st.session_state.view = "home"
if 'agent_status' not in st.session_state:
st.session_state.agent_status = "Idle"
if 'active_process' not in st.session_state:
st.session_state.active_process = None
if 'log_file' not in st.session_state:
st.session_state.log_file = None
if 'final_log_content' not in st.session_state:
st.session_state.final_log_content = ""
if 'output_files' not in st.session_state:
st.session_state.output_files =[]
if 'files_before_run' not in st.session_state:
st.session_state.files_before_run = set()
if 'db_events' not in st.session_state:
st.session_state.db_events =[]
if 'db_selected_event' not in st.session_state:
st.session_state.db_selected_event = None
if 'db_run_outputs' not in st.session_state:
st.session_state.db_run_outputs =[]
# Intent Flags
if 'start_crawl4ai_flag' not in st.session_state:
st.session_state.start_crawl4ai_flag = False
if 'start_calendarcrawl_flag' not in st.session_state:
st.session_state.start_calendarcrawl_flag = False
if 'start_gemini_flag' not in st.session_state:
st.session_state.start_gemini_flag = False
# --- Helper Functions ---
def read_log_file():
if st.session_state.log_file and Path(st.session_state.log_file).exists():
try:
with open(st.session_state.log_file, 'r', encoding='utf-8', errors='ignore') as f:
return f.read()
except Exception:
return "Reading log file..."
return "Process has not started yet."
def extract_progress(log_content):
"""Parses the log content to find the current progress (e.g., '1/50')."""
match = re.findall(r"\[STARTING MISSION\]\s+(\d+)/(\d+)", log_content)
if match:
current, total = match[-1]
return int(current), int(total)
return 0, 0
def get_files_in_dir(directory):
dir_path = Path(directory)
if not dir_path.is_dir():
dir_path.mkdir(exist_ok=True)
return {str(f) for f in dir_path.glob("*.csv")}
def start_agent_process(command):
st.session_state.output_files =[]
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
st.session_state.log_file = LOG_DIR / f"run_{timestamp}.log"
st.session_state.final_log_content = ""
st.session_state.files_before_run = get_files_in_dir(OUTPUT_DIR)
current_env = os.environ.copy()
try:
with open(st.session_state.log_file, 'w', encoding='utf-8') as log_f:
process = subprocess.Popen(
command,
stdout=log_f,
stderr=subprocess.STDOUT,
text=True,
encoding='utf-8',
errors='replace',
env=current_env
)
st.session_state.active_process = process
st.session_state.agent_status = "Running"
except Exception as e:
st.error(f"Error during agent setup: {e}")
def change_view(view_name):
st.session_state.view = view_name
# --- ACTION PROCESSING LOGIC ---
def process_crawl4ai_start():
if st.session_state.agent_status == "Running": return
input_method = st.session_state.get('crawl4ai_input_method', 'File Upload')
max_events_str_web = st.session_state.get('max_events_web', "")
output_filename = st.session_state.get('crawl4ai_output_filename', "")
temp_input_path = TEMP_DIR / "races.json"
input_ready = False
if input_method == "File Upload":
input_file = st.session_state.get('web_agent_uploader')
if not input_file:
st.error("Please upload an input data file.")
return
try:
file_contents = input_file.getvalue()
if input_file.name.lower().endswith('.csv'):
df = pd.read_csv(io.BytesIO(file_contents))
df.to_json(temp_input_path, orient='records', indent=4)
elif input_file.name.lower().endswith('.xlsx'):
df = pd.read_excel(io.BytesIO(file_contents))
df.to_json(temp_input_path, orient='records', indent=4)
else:
with open(temp_input_path, "wb") as f: f.write(file_contents)
input_ready = True
except Exception as e: st.error(f"Error preparing input file: {e}")
else:
manual_event_names = st.session_state.get('manual_event_names', "")
event_type = st.session_state.get('manual_event_type', "Run")
if not manual_event_names.strip():
st.error("Please enter at least one event name.")
return
try:
event_list =[name.strip() for name in manual_event_names.strip().split('\n') if name.strip()]
races_data =[{"Festival": name, "Type": event_type} for name in event_list]
with open(temp_input_path, 'w', encoding='utf-8') as f: json.dump(races_data, f, indent=4)
input_ready = True
except Exception as e: st.error(f"Error preparing manual input: {e}")
if input_ready:
command =[sys.executable, "-u", "main.py", "--mode", "web_analyst", "--output-dir", OUTPUT_DIR, "--input-file", str(temp_input_path)]
if max_events_str_web.isdigit(): command.extend(["--max-events", max_events_str_web])
if output_filename: command.extend(["--output-filename", output_filename])
if st.session_state.get("enable_fallback", False): command.append("--enable-fallback")
start_agent_process(command)
st.rerun()
def process_calendarcrawl_start():
if st.session_state.agent_status == "Running": return
calendar_mode = st.session_state.get('cal_agent_mode', 'Crawl Cycling Calendar')
max_events_str_cal = st.session_state.get('max_events_cal', "")
output_filename = st.session_state.get('cal_output_filename', "")
uploaded_files_cal = st.session_state.get('cal_agent_uploader',[])
if calendar_mode == "Process Default Websites" and not uploaded_files_cal:
st.error("Please upload at least one JSON file for this mode.")
return
temp_input_paths_cal =[]
if uploaded_files_cal:
for up_file in uploaded_files_cal:
path = TEMP_DIR / up_file.name
with open(path, "wb") as f: f.write(up_file.getvalue())
temp_input_paths_cal.append(str(path))
mode_flag = "calendar_crawl" if calendar_mode == "Crawl Cycling Calendar" else "default_websites"
command =[sys.executable, "-u", "main.py", "--mode", mode_flag, "--output-dir", OUTPUT_DIR]
if temp_input_paths_cal:
command.append("--input-files")
command.extend(temp_input_paths_cal)
if max_events_str_cal.isdigit(): command.extend(["--max-events", max_events_str_cal])
if output_filename: command.extend(["--output-filename", output_filename])
if st.session_state.get("enable_fallback", False): command.append("--enable-fallback")
start_agent_process(command)
st.rerun()
def process_gemini_start():
if st.session_state.agent_status == "Running": return
uploaded_images = st.session_state.get('gemini_uploader',[])
output_filename = st.session_state.get('gemini_output_filename', "")
if not uploaded_images:
st.error("Please upload at least one image.")
return
run_id = uuid.uuid4()
image_input_path = TEMP_DIR / f"gemini_input_{run_id}"
image_input_path.mkdir(exist_ok=True)
for image in uploaded_images:
with open(image_input_path / image.name, "wb") as f: f.write(image.getbuffer())
command =[sys.executable, "-u", "gemini.py", "--output-dir", OUTPUT_DIR, "--input-dir", str(image_input_path)]
if output_filename: command.extend(["--output-filename", output_filename])
if st.session_state.get("enable_fallback", False): command.append("--enable-fallback")
start_agent_process(command)
st.rerun()
# --- UI RENDER FUNCTIONS ---
def show_home_screen():
st.title("🤖 Welcome to the Crawl4AI Unified Agent System")
st.markdown(f"**Version:** `{APP_VERSION}`")
st.write("---")
if not SECRETS_LOADED:
st.error("**Hugging Face Secrets Not Loaded.** Ensure keys like 'MISTRAL_API_KEY' are set in Space Settings.", icon="🔒")
elif not SUPABASE_CONFIGURED:
st.warning("**Supabase Not Configured.** The application will run in local-only mode.", icon="⚠️")
st.subheader("🔍 Select a Module to Begin")
st.write("")
row1_col1, row1_col2 = st.columns(2, gap="large")
row2_col1, row2_col2 = st.columns(2, gap="large")
with row1_col1:
with st.container(border=True):
st.header("📘 Web Research Agent")
st.markdown("**(Crawl4AI)**")
st.write("Provide event names, and this agent will autonomously search trusted sources, validate information, and extract structured data using deep AI analysis.")
st.button("Launch Web Agent", on_click=change_view, args=("crawl4ai",), use_container_width=True, type="primary", disabled=not SECRETS_LOADED)
with row1_col2:
with st.container(border=True):
st.header("📄 Pre-scraped Data Processor")
st.markdown("**(CalendarCrawl)**")
st.write("Upload pre-scraped JSON files or supply known event calendar URLs. The processor extracts structured event details using the CalendarCrawl engine.")
st.button("Launch Data Processor", on_click=change_view, args=("calendarcrawl",), use_container_width=True, type="primary", disabled=not SECRETS_LOADED)
with row2_col1:
with st.container(border=True):
st.header("🖼️ Gemini Image Processor")
st.markdown("**(Image Analysis)**")
st.write("Upload event posters or banners. This module uses Google Gemini Vision to extract all essential event details with intelligent image analysis.")
st.button("Launch Image Processor", on_click=change_view, args=("gemini",), use_container_width=True, type="primary", disabled=not SECRETS_LOADED)
with row2_col2:
with st.container(border=True):
st.header("🗂️ Database Manager")
st.markdown("**(Supabase-backed store)**")
st.write("View, manage, and delete persistent knowledge and cached crawl data stored securely in your Supabase project.")
st.button("Open Database Manager", on_click=change_view, args=("db_manager",), use_container_width=True, type="primary", disabled=not SUPABASE_CONFIGURED)
def show_agent_ui():
is_running = st.session_state.agent_status == "Running"
with st.sidebar:
st.header("Configuration Status")
if SECRETS_LOADED:
st.success("✅ Hugging Face Secrets loaded.")
else:
st.error("❌ Hugging Face Secrets missing.")
if SUPABASE_CONFIGURED:
st.success("☁️ Supabase configured.")
else:
st.warning("⚠️ Supabase not configured.")
st.markdown("---")
# STRICT DB FALLBACK TOGGLE
st.session_state.enable_fallback = st.checkbox(
"Enable Schema Fallback",
value=st.session_state.get('enable_fallback', False),
help="If disabled (default), execution will fail if Database2 context is unavailable or incomplete. Check this to use hardcoded local schemas as a fallback."
)
st.markdown("---")
if st.button("🔒 Logout"):
st.session_state.password_correct = False
st.rerun()
col1, col2 = st.columns([0.45, 0.55], gap="large")
with col1:
st.button("⬅️ Back to Home", on_click=change_view, args=("home",), disabled=is_running)
st.header("Agent Control Panel")
if st.session_state.view == "crawl4ai": render_crawl4ai_controls()
elif st.session_state.view == "calendarcrawl": render_calendarcrawl_controls()
elif st.session_state.view == "gemini": render_gemini_controls()
st.header("📂 Results")
with st.container(border=True):
if st.session_state.agent_status == "Finished":
if st.session_state.output_files:
st.success("Processing finished!")
for file_path in st.session_state.output_files:
file = Path(file_path)
with open(file, "rb") as fp:
st.download_button(label=f"📄 Download {file.name}", data=fp, file_name=file.name, mime="text/csv", key=f"dl_{file.name}", use_container_width=True)
else:
st.warning("Process finished, but no new output files were generated.")
else:
st.info("Results from the current run will appear here after completion.")
with col2:
st.header("Live Status & Logs")
progress_container = st.empty()
status = st.session_state.agent_status
if status == "Idle": st.info("📊 **Status:** Waiting to start a process.")
elif status == "Running": st.warning("⚙️ **Status:** Running... Logs refresh automatically.")
elif status == "Finished": st.success("✅ **Status:** Process finished successfully.")
elif status == "Terminated": st.error("🛑 **Status:** Process stopped by user.")
elif status == "Error": st.error("🔥 **Status:** Process failed with an error. Check logs.")
if is_running:
if st.button("⏹️ Stop Active Process", use_container_width=True):
if st.session_state.active_process:
st.session_state.active_process.terminate()
time.sleep(1)
st.session_state.agent_status = "Terminated"
st.session_state.active_process = None
st.rerun()
log_content = st.session_state.final_log_content if status != "Running" else read_log_file()
if status == "Running":
curr, total = extract_progress(log_content)
if total > 0:
progress_container.progress(curr / total, text=f"Processing Event {curr} of {total}")
elif curr > 0:
progress_container.info(f"Processing Event {curr}...")
st.text_area("Log Output", value=log_content, height=500, disabled=True, key="log_area")
def show_db_manager_ui():
st.button("⬅️ Back to Home", on_click=change_view, args=("home",))
st.title("🗃️ Database & Cache Manager")
st.info("Here you can view data saved in Supabase and manage both local and remote caches.")
tab1, tab2 = st.tabs(["Knowledge Cache Manager", "Run Outputs Manager"])
with tab1:
col1, col2 = st.columns([0.4, 0.6], gap="large")
with col1:
st.subheader("Cached Events")
if st.button("🔄 Refresh Event List"): st.session_state.db_events =[]
if not st.session_state.db_events:
with st.spinner("Fetching events from Supabase..."): st.session_state.db_events = db_manager.list_all_events()
if not st.session_state.db_events: st.warning("No events found in the Supabase knowledge cache.")
else:
with st.container(height=400):
st.radio("Select an event to manage:", options=st.session_state.db_events, key="db_selected_event", label_visibility="collapsed")
st.write("---")
st.subheader("Global Cache Utilities")
with st.container(border=True):
if st.button("🗑️ Clear ALL Local Crawl Cache"):
try:
if Path(CRAWL_CACHE_DIR).exists(): shutil.rmtree(CRAWL_CACHE_DIR)
Path(CRAWL_CACHE_DIR).mkdir(exist_ok=True)
st.toast("Cleared Local Crawl Cache.", icon="✅")
except Exception as e: st.error(f"Error: {e}")
if st.button("🗑️ Clear ALL Local Knowledge Cache"):
try:
if Path(KNOWLEDGE_CACHE_DIR).exists(): shutil.rmtree(KNOWLEDGE_CACHE_DIR)
Path(KNOWLEDGE_CACHE_DIR).mkdir(exist_ok=True)
st.toast("Cleared Local Knowledge Cache.", icon="✅")
except Exception as e: st.error(f"Error: {e}")
if st.button("🗑️ Clear ENTIRE Local Vector DB"):
try:
if Path(VECTOR_DB_PATH).exists(): shutil.rmtree(VECTOR_DB_PATH)
Path(VECTOR_DB_PATH).mkdir(exist_ok=True)
st.toast("Cleared Local Vector DB.", icon="✅")
except Exception as e: st.error(f"Error: {e}")
with col2:
st.subheader("Event Details & Management")
if st.session_state.db_selected_event:
with st.container(border=True):
with st.spinner(f"Fetching details for '{st.session_state.db_selected_event}'..."):
event_data = db_manager.get_event_details(st.session_state.db_selected_event)
if event_data:
st.subheader("Supabase Knowledge Cache")
st.json(event_data, expanded=False)
st.write("---")
st.subheader("Local Cache Management")
if st.button("Clear Local Cache for this Event"):
with st.spinner(f"Clearing local cache for '{st.session_state.db_selected_event}'..."):
success, message = db_manager.clear_local_event_cache(st.session_state.db_selected_event)
if success: st.success(message)
else: st.error(message)
st.write("---")
st.subheader("⚠️ Danger Zone: Supabase Deletion")
confirm_delete = st.checkbox(f"I want to permanently delete all data for '{st.session_state.db_selected_event}' from Supabase.", key="delete_confirm")
if st.button("🔥 Hard Delete Event from Supabase", type="primary", disabled=not confirm_delete):
with st.spinner(f"Performing hard delete for '{st.session_state.db_selected_event}'..."):
success, message = db_manager.delete_event_from_supabase(st.session_state.db_selected_event)
if success:
st.success(message)
st.session_state.db_events =[]
st.session_state.db_selected_event = None
st.rerun()
else:
st.error(message)
else:
st.error("Could not retrieve details for the selected event.")
else:
st.info("Select an event from the list to see its cached data.")
with tab2:
st.subheader("Stored Run Outputs")
if st.button("🔄 Refresh Run Outputs"): st.session_state.db_run_outputs =[]
if not st.session_state.db_run_outputs:
with st.spinner("Fetching run outputs from Supabase..."):
st.session_state.db_run_outputs = db_manager.list_run_outputs()
if not st.session_state.db_run_outputs: st.warning("No run outputs found in Supabase.")
else:
for run in st.session_state.db_run_outputs:
with st.container(border=True):
st.markdown(f"**File:** `{run['filename']}`")
st.markdown(f"**Mode:** `{run['agent_mode']}` | **Events:** `{run['event_count']}` | **Date:** `{datetime.fromisoformat(run['created_at']).strftime('%Y-%m-%d %H:%M')}`")
c1, c2 = st.columns(2)
with c1:
df = pd.DataFrame(run['run_data'])
csv_data = df.to_csv(index=False).encode('utf-8')
st.download_button(label="📥 Download CSV", data=csv_data, file_name=run['filename'], mime="text/csv", key=f"dl_run_{run['id']}")
with c2:
if st.button("❌ Delete this Output", key=f"delete_run_{run['id']}", type="primary"):
success, message = db_manager.delete_run_output(run['id'])
if success:
st.toast(f"Deleted {run['filename']}", icon="✅")
st.session_state.db_run_outputs =[]
st.rerun()
else:
st.error(message)
def render_crawl4ai_controls():
st.subheader("🌐 Web Research Agent")
st.info("This agent takes event names, searches the web, and performs deep analysis.")
is_running = st.session_state.agent_status == "Running"
with st.container(border=True):
st.subheader("1. Input Method")
st.radio("Choose how to provide event names:",["File Upload", "Manual Entry"], horizontal=True, label_visibility="collapsed", key='crawl4ai_input_method', disabled=is_running)
if st.session_state.crawl4ai_input_method == "File Upload":
st.file_uploader("Upload Event List File (JSON/CSV/XLSX)", type=['json', 'csv', 'xlsx'], key="web_agent_uploader", disabled=is_running, label_visibility="collapsed")
else:
st.subheader("Enter Event Details")
st.selectbox("Event Type (for all events below)", options=CHOICE_OPTIONS["type"], index=4, key='manual_event_type', disabled=is_running)
st.text_area("Event Names (one per line)", placeholder="Event Name 1\nEvent Name 2", height=150, key='manual_event_names', disabled=is_running)
st.text_input("Max events to process (optional)", key="max_events_web", help="Limit the number of events to process from the input.", disabled=is_running)
st.text_input("Custom Output Filename (optional)", key="crawl4ai_output_filename", help="e.g., 'My_Marathon_Run_Output'", disabled=is_running)
with st.container(border=True):
def set_start_flag(): st.session_state.start_crawl4ai_flag = True
st.button("▶️ Start Web Research Agent", on_click=set_start_flag, disabled=is_running or not SECRETS_LOADED, use_container_width=True, type="primary")
def render_calendarcrawl_controls():
st.subheader("📄 Pre-scraped Data Processor")
st.info("This agent processes data from pre-scraped files or from a specific calendar URL.")
is_running = st.session_state.agent_status == "Running"
with st.container(border=True):
st.subheader("1. Mode")
st.selectbox("Select Mode",["Crawl Cycling Calendar", "Process Default Websites"], key='cal_agent_mode', disabled=is_running)
st.text_input("Max events to process (optional)", key="max_events_cal", help="Limit the number of events to process.", disabled=is_running)
st.text_input("Custom Output Filename (optional)", key="cal_output_filename", help="e.g., 'Cycling_Calendar_Output'", disabled=is_running)
with st.container(border=True):
st.subheader("2. Inputs")
if st.session_state.get('cal_agent_mode') == "Process Default Websites":
st.file_uploader("Upload Pre-scraped JSON File(s)", type=['json'], accept_multiple_files=True, key="cal_agent_uploader", disabled=is_running)
else:
st.success("This mode will automatically crawl `audaxindia.in`.")
with st.container(border=True):
def set_start_flag(): st.session_state.start_calendarcrawl_flag = True
st.button("▶️ Start Pre-scraped Data Processor", on_click=set_start_flag, disabled=is_running or not SECRETS_LOADED, use_container_width=True, type="primary")
def render_gemini_controls():
st.subheader("🖼️ Gemini Image Processor")
st.info("This agent analyzes images of event posters to extract key details.")
is_running = st.session_state.agent_status == "Running"
with st.container(border=True):
st.subheader("1. Inputs")
st.file_uploader("Upload Image Files", type=['png', 'jpg', 'jpeg'], accept_multiple_files=True, key='gemini_uploader', disabled=is_running)
st.text_input("Custom Output Filename (optional)", key="gemini_output_filename", help="e.g., 'Poster_Analysis_Output'", disabled=is_running)
with st.container(border=True):
def set_start_flag(): st.session_state.start_gemini_flag = True
st.button("▶️ Start Gemini Processor", on_click=set_start_flag, disabled=is_running or not SECRETS_LOADED, use_container_width=True, type="primary")
# --- Main Logic Flow ---
initialize_session_state()
if st.session_state.view == "home":
show_home_screen()
elif st.session_state.view == "db_manager":
show_db_manager_ui()
else:
show_agent_ui()
# Evaluate action flags
if st.session_state.start_crawl4ai_flag:
st.session_state.start_crawl4ai_flag = False
process_crawl4ai_start()
elif st.session_state.start_calendarcrawl_flag:
st.session_state.start_calendarcrawl_flag = False
process_calendarcrawl_start()
elif st.session_state.start_gemini_flag:
st.session_state.start_gemini_flag = False
process_gemini_start()
# Handle Background Process Status
if st.session_state.agent_status == "Running":
process = st.session_state.active_process
if process and process.poll() is not None:
time.sleep(1)
st.session_state.final_log_content = read_log_file()
return_code = process.poll()
st.session_state.agent_status = "Finished" if return_code == 0 else "Error"
if return_code == 0:
files_after_run = get_files_in_dir(OUTPUT_DIR)
st.session_state.output_files = sorted(list(files_after_run - st.session_state.files_before_run))
st.session_state.active_process = None
st.rerun()
else:
time.sleep(5)
st.rerun()