# app.py
"""
Streamlit frontend application for the Tensorus platform.
New UI structure with top navigation and Nexus Dashboard.
"""
import streamlit as st
import json
import time
import requests # Needed for ui_utils functions if integrated
import logging # Needed for ui_utils functions if integrated
import torch # Needed for integrated tensor utils
from typing import List, Dict, Any, Optional, Union, Tuple # Needed for integrated tensor utils
from pages.pages_shared_utils import get_api_status, get_agent_status, get_datasets # Updated imports
# Work around a Streamlit bug where inspecting `torch.classes` during module
# watching can raise a `RuntimeError`. Removing the module from `sys.modules`
# prevents Streamlit's watcher from trying to access it.
import sys
if "torch.classes" in sys.modules:
del sys.modules["torch.classes"]
# --- Page Configuration ---
st.set_page_config(
page_title="Tensorus Platform",
page_icon="🧊",
layout="wide",
initial_sidebar_state="collapsed" # Collapse sidebar as nav is now at top
)
# --- Configure Logging (Optional but good practice) ---
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# --- Integrated Tensor Utilities (Preserved) ---
def _validate_tensor_data(data: List[Any], shape: List[int]):
"""
Validates if the nested list structure of 'data' matches the 'shape'.
Raises ValueError on mismatch. (Optional validation)
"""
if not shape:
if not isinstance(data, (int, float)): raise ValueError("Scalar tensor data must be a single number.")
return True
if not isinstance(data, list): raise ValueError(f"Data for shape {shape} must be a list.")
expected_len = shape[0]
if len(data) != expected_len: raise ValueError(f"Dimension 0 mismatch: Expected {expected_len}, got {len(data)} for shape {shape}.")
if len(shape) > 1:
for item in data: _validate_tensor_data(item, shape[1:])
elif len(shape) == 1:
if not all(isinstance(x, (int, float)) for x in data): raise ValueError("Innermost list elements must be numbers.")
return True
def list_to_tensor(shape: List[int], dtype_str: str, data: Union[List[Any], int, float]) -> torch.Tensor:
"""
Converts a Python list (potentially nested) or scalar into a PyTorch tensor
with the specified shape and dtype.
"""
try:
dtype_map = {
'float32': torch.float32, 'float': torch.float,
'float64': torch.float64, 'double': torch.double,
'int32': torch.int32, 'int': torch.int,
'int64': torch.int64, 'long': torch.long,
'bool': torch.bool
}
torch_dtype = dtype_map.get(dtype_str.lower())
if torch_dtype is None: raise ValueError(f"Unsupported dtype string: {dtype_str}")
tensor = torch.tensor(data, dtype=torch_dtype)
if list(tensor.shape) != shape:
logger.debug(f"Initial tensor shape {list(tensor.shape)} differs from target {shape}. Attempting reshape.")
try:
tensor = tensor.reshape(shape)
except RuntimeError as reshape_err:
raise ValueError(f"Created tensor shape {list(tensor.shape)} != requested {shape} and reshape failed: {reshape_err}") from reshape_err
return tensor
except (TypeError, ValueError) as e:
logger.error(f"Error converting list to tensor: {e}. Shape: {shape}, Dtype: {dtype_str}")
raise ValueError(f"Failed tensor conversion: {e}") from e
except Exception as e:
logger.exception(f"Unexpected error during list_to_tensor: {e}")
raise ValueError(f"Unexpected tensor conversion error: {e}") from e
def tensor_to_list(tensor: torch.Tensor) -> Tuple[List[int], str, List[Any]]:
"""
Converts a PyTorch tensor back into its shape, dtype string, and nested list representation.
"""
if not isinstance(tensor, torch.Tensor):
raise TypeError("Input must be a torch.Tensor")
shape = list(tensor.shape)
dtype_str = str(tensor.dtype).split('.')[-1]
data = tensor.tolist()
return shape, dtype_str, data
# --- Helper functions for dashboard (can be expanded) ---
def get_total_tensors_placeholder():
# For now, as this endpoint is hypothetical for this task
return "N/A"
@st.cache_data(ttl=300)
def get_active_datasets_placeholder():
datasets = get_datasets()
if datasets: # get_datasets returns [] on error or if no datasets
return str(len(datasets))
return "Error"
@st.cache_data(ttl=60)
def get_agents_online_placeholder():
agent_data = get_agent_status()
if agent_data:
try:
# Assuming agent_data is a dict like {'agent_id': {'status': 'running', ...}}
# or {'agent_id': {'running': True, ...}}
online_agents = sum(1 for agent in agent_data.values()
if agent.get('running') is True or str(agent.get('status', '')).lower() == 'running')
total_agents = len(agent_data)
return f"{online_agents}/{total_agents} Online"
except Exception as e:
logger.error(f"Error processing agent data for dashboard: {e}")
return "Error"
return "N/A" # If agent_data is None
# --- CSS Styles ---
# Renaming app.py's specific CSS loader to avoid confusion with the shared one.
def load_app_specific_css():
# This function now only loads styles specific to the Nexus Dashboard content in app.py
# General styles (body, .stApp, nav, common-card, etc.) are in pages_shared_utils.load_shared_css()
st.markdown("""
""", unsafe_allow_html=True)
# --- Page Functions ---
def nexus_dashboard_content():
# Uses .dashboard-title for its main heading
st.markdown('
Tensorus Nexus
', unsafe_allow_html=True)
# System Health & Key Metrics
# Uses .metric-card-container for the overall layout
st.markdown('
""", unsafe_allow_html=True)
# Card 4: API Status
@st.cache_data(ttl=30)
def cached_get_api_status():
return get_api_status()
api_ok, _ = cached_get_api_status()
api_status_text_val = "Connected" if api_ok else "Disconnected"
# Add specific class for API status icon coloring based on shared status styles
api_status_icon_class = "api-status-connected" if api_ok else "api-status-disconnected"
api_icon_char = "✔️" if api_ok else "❌"
st.markdown(f"""
{api_icon_char}
API Status
{api_status_text_val}
""", unsafe_allow_html=True)
st.markdown('
', unsafe_allow_html=True) # Close metric-card-container
# Agent Activity Feed
# Uses .activity-feed-container and h2 (which is styled by shared CSS)
st.markdown('
', unsafe_allow_html=True)
st.markdown('
Recent Agent Activity
', unsafe_allow_html=True)
# Placeholder activity items
activity_items = [
{"timestamp": "2023-10-27 10:05:15", "agent": "IngestionAgent", "action": "added 'img_new.png' to 'raw_images'"},
{"timestamp": "2023-10-27 10:02:30", "agent": "RLAgent", "action": "completed training cycle, reward: 75.2"},
{"timestamp": "2023-10-27 09:55:48", "agent": "MonitoringAgent", "action": "detected high CPU usage on node 'compute-01'"},
{"timestamp": "2023-10-27 09:45:10", "agent": "IngestionAgent", "action": "processed batch of 100 sensor readings"},
]
for item in activity_items:
st.markdown(f"""
', unsafe_allow_html=True) # Close activity-feed-container
# --- Main Application ---
# Import the shared CSS loader
try:
from pages.pages_shared_utils import load_css as load_shared_css
except ImportError:
st.error("Failed to import shared CSS loader. Page styling will be incomplete.")
def load_shared_css(): pass # Dummy function
def main():
load_shared_css() # Load shared styles first
load_app_specific_css() # Then load app-specific styles (for dashboard)
# Initialize session state for current page if not set
if 'current_page' not in st.session_state:
st.session_state.current_page = "Nexus Dashboard"
# --- Top Navigation Bar ---
nav_items = {
"Nexus Dashboard": "Nexus Dashboard",
"Agents": "Agents",
"Explorer": "Explorer",
"Query Hub": "Query Hub",
"API Docs": "API Docs"
}
nav_html_parts = [f'
🧊 Tensorus
')
st.markdown("".join(nav_html_parts), unsafe_allow_html=True)
# Handle page selection clicks (alternative to query_params if that proves problematic)
# This part is tricky with pure st.markdown links.
# The query_params approach is generally preferred for web-like navigation.
# --- Content Area ---
# The main app.py now acts as a router to other pages or displays dashboard content directly.
if st.session_state.current_page == "Nexus Dashboard":
nexus_dashboard_content()
elif st.session_state.current_page == "Agents":
st.switch_page("pages/control_panel_v2.py")
elif st.session_state.current_page == "Explorer":
st.switch_page("pages/data_explorer_v2.py")
elif st.session_state.current_page == "Query Hub":
st.switch_page("pages/nql_chatbot_v2.py")
elif st.session_state.current_page == "API Docs":
st.switch_page("pages/api_playground_v2.py")
else:
# Default to Nexus Dashboard if current_page is unrecognized
st.session_state.current_page = "Nexus Dashboard"
nexus_dashboard_content()
# It's good practice to trigger a rerun if state was corrected
st.rerun()
if __name__ == "__main__":
# --- Initialize Old Session State Keys (to avoid errors if they are still used by preserved code) ---
# This should be phased out as those sections are rebuilt.
if 'agent_status' not in st.session_state: st.session_state.agent_status = None
if 'datasets' not in st.session_state: st.session_state.datasets = []
if 'selected_dataset' not in st.session_state: st.session_state.selected_dataset = None
if 'dataset_preview' not in st.session_state: st.session_state.dataset_preview = None
if 'explorer_result' not in st.session_state: st.session_state.explorer_result = None
if 'nql_response' not in st.session_state: st.session_state.nql_response = None
main()