Spaces:
Running
Running
File size: 16,696 Bytes
edfa748 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 | # 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("""
<style>
/* Nexus Dashboard Specific Styles */
.dashboard-title { /* Custom title for the dashboard */
color: #e0e0ff; /* Light purple/blue, matching shared h1 */
text-align: center;
margin-top: 1rem; /* Standardized margin */
margin-bottom: 2rem;
font-size: 2.8em; /* Slightly larger for main dashboard title */
font-weight: bold;
}
/* Metric Cards Container for Dashboard */
.metric-card-container {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
gap: 20px;
padding: 0 1rem;
}
/* Individual Metric Card - inherits from .common-card (defined in shared_utils) */
/* This specific .metric-card class is for dashboard cards if they need further specialization */
.metric-card {
/* Inherits background, border, padding, shadow, transition from .common-card */
flex: 1 1 220px; /* Adjusted flex basis */
min-width: 200px;
max-width: 300px;
text-align: center;
}
/* .metric-card:hover is inherited from .common-card:hover */
.metric-card .icon { /* Specific styling for icons within dashboard metric cards */
font-size: 2.8em; /* Slightly larger icon for dashboard */
margin-bottom: 0.5rem; /* Tighter spacing */
/* color is inherited from .common-card .icon or can be overridden here */
}
.metric-card h3 { /* Metric card titles */
/* color, font-size, margin-bottom, font-weight inherited from .common-card h3 */
/* No specific overrides here unless needed for dashboard metric cards */
}
.metric-card p.metric-value { /* Specific class for the main value display */
font-size: 2em; /* Larger font for the metric value */
font-weight: bold;
color: #ffffff; /* White color for emphasis */
margin-top: 0.25rem; /* Adjust as needed */
margin-bottom: 0;
}
/* Specific status icon colors for API status card in dashboard */
.metric-card.api-status-connected .icon { color: #50C878; } /* Emerald Green */
.metric-card.api-status-disconnected .icon { color: #FF6961; } /* Pastel Red */
/* Activity Feed Styles for Dashboard */
.activity-feed-container { /* Container for the activity feed section */
margin-top: 2.5rem;
padding: 0 1.5rem;
}
/* .activity-feed-container h2 is covered by shared h2 styles */
.activity-item { /* Individual item in the feed */
background-color: #1e2a47; /* Slightly lighter than common-card, for variety */
padding: 0.85rem 1.25rem; /* Adjusted padding */
border-radius: 6px;
margin-bottom: 0.6rem; /* Slightly more space */
font-size: 0.95em;
border-left: 4px solid #3a6fbf; /* Accent Blue border */
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.activity-item .timestamp { /* Timestamp within an activity item */
color: #8080af; /* Muted purple for timestamp */
font-weight: bold;
font-size: 0.85em; /* Smaller timestamp */
margin-right: 0.75em; /* More space after timestamp */
}
.activity-item strong { /* Agent name or key part of activity */
color: #b0b0df; /* Tertiary heading color for emphasis */
}
</style>
""", unsafe_allow_html=True)
# --- Page Functions ---
def nexus_dashboard_content():
# Uses .dashboard-title for its main heading
st.markdown('<h1 class="dashboard-title">Tensorus Nexus</h1>', unsafe_allow_html=True)
# System Health & Key Metrics
# Uses .metric-card-container for the overall layout
st.markdown('<div class="metric-card-container">', unsafe_allow_html=True)
# Card 1: Total Tensors
total_tensors_val = get_total_tensors_placeholder()
st.markdown(f"""
<div class="common-card metric-card">
<div class="icon">โ๏ธ</div>
<h3>Total Tensors</h3>
<p class="metric-value">{total_tensors_val}</p>
</div>
""", unsafe_allow_html=True)
# Card 2: Active Datasets
active_datasets_val = get_active_datasets_placeholder()
st.markdown(f"""
<div class="common-card metric-card">
<div class="icon">๐</div>
<h3>Active Datasets</h3>
<p class="metric-value">{active_datasets_val}</p>
</div>
""", unsafe_allow_html=True)
# Card 3: Agents Online
agents_online_val = get_agents_online_placeholder()
st.markdown(f"""
<div class="common-card metric-card">
<div class="icon">๐ค</div>
<h3>Agents Online</h3>
<p class="metric-value">{agents_online_val}</p>
</div>
""", 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"""
<div class="common-card metric-card {api_status_icon_class}">
<div class="icon">{api_icon_char}</div>
<h3>API Status</h3>
<p class="metric-value">{api_status_text_val}</p>
</div>
""", unsafe_allow_html=True)
st.markdown('</div>', 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('<div class="activity-feed-container">', unsafe_allow_html=True)
st.markdown('<h2>Recent Agent Activity</h2>', 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"""
<div class="activity-item">
<span class="timestamp">[{item['timestamp']}]</span>
<strong>{item['agent']}:</strong> {item['action']}
</div>
""", unsafe_allow_html=True)
st.markdown('</div>', 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'<div class="topnav-container"><span class="logo">๐ง Tensorus</span><nav>']
for page_id, page_name in nav_items.items():
active_class = "active" if st.session_state.current_page == page_id else ""
# Use st.button an invisible character as key to make Streamlit rerun and update session state
# This is a common workaround for making nav links update state
# We'll use a more robust JavaScript approach if this is problematic, but st.query_params is better
# Using query_params for navigation state is more robust
# Check if query_params for page is set, if so, it overrides session_state
query_params = st.query_params.to_dict()
if "page" in query_params and query_params["page"] in nav_items:
st.session_state.current_page = query_params["page"]
# Clear the query param after use to avoid it sticking on manual refresh
# However, for deeplinking, we might want to keep it.
# For now, let's allow it to persist. To clear: st.query_params.clear()
# Construct the link with query_params
nav_html_parts.append(
f'<a href="?page={page_id}" class="{active_class}" target="_self">{page_name}</a>'
)
nav_html_parts.append('</nav></div>')
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()
|