core / pages /pages_shared_utils.py
tensorus's picture
Upload 83 files
edfa748 verified
# pages/pages_shared_utils.py
"""
Shared utility functions for Streamlit pages.
Copied/adapted from app.py to avoid complex import issues.
"""
import streamlit as st
import requests
import logging
import os # Added import
from typing import Optional, List, Dict, Any # Added for new functions
logger = logging.getLogger(__name__)
API_BASE_URL = os.getenv("API_BASE_URL", "http://127.0.0.1:7860") # Changed API_BASE_URL
def load_css():
"""Loads the main CSS styles. Assumes app.py's CSS content."""
st.markdown("""
<style>
/* --- Shared Base Styles for Tensorus Platform (Nexus Theme) --- */
/* General Page Styles */
body {
font-family: 'Arial', 'Helvetica Neue', 'Helvetica', sans-serif;
line-height: 1.6;
}
.stApp { /* Main Streamlit app container */
background-color: #0a0f2c; /* Primary Background: Dark blue/purple */
color: #e0e0e0; /* Default Text Color: Light grey */
}
/* Headings & Titles */
h1, .stTitle { /* Main page titles */
color: #d0d0ff !important; /* Primary Heading Color: Light purple/blue */
font-weight: bold !important;
}
h2, .stSubheader { /* Section headers */
color: #c0c0ef !important; /* Secondary Heading Color */
font-weight: bold !important;
border-bottom: 1px solid #3a3f5c; /* Accent Border for separation */
padding-bottom: 0.3rem;
margin-top: 1.5rem;
margin-bottom: 1rem;
}
h3 { /* General h3, often used in st.markdown */
color: #b0b0df !important; /* Tertiary Heading Color */
font-weight: bold !important;
}
.stMarkdown p, .stText, .stListItem { /* General text elements */
color: #c0c0dd; /* Softer light text */
font-size: 1rem;
}
.stCaption, caption { /* Streamlit captions and HTML captions */
font-size: 0.85rem !important;
color: #a0a0c0 !important; /* Muted color for captions */
}
/* Custom Top Navigation Bar */
.topnav-container {
background-color: #1a1f3c; /* Nav Background: Slightly darker than page */
padding: 0.5rem 1rem;
border-bottom: 1px solid #3a3f5c; /* Accent Border */
display: flex;
justify-content: flex-start;
align-items: center;
position: sticky; top: 0; z-index: 1000; /* Ensure it's on top */
width: 100%;
box-sizing: border-box;
}
.topnav-container .logo {
font-size: 1.5em;
font-weight: bold;
color: #d0d0ff; /* Primary Heading Color for logo */
margin-right: 2rem;
}
.topnav-container nav a {
color: #c0c0ff; /* Lighter Text for Nav Links */
padding: 0.75rem 1rem;
text-decoration: none;
font-weight: 500;
margin-right: 0.5rem;
border-radius: 4px;
transition: background-color 0.2s ease, color 0.2s ease;
}
.topnav-container nav a:hover {
background-color: #2a2f4c; /* Nav Link Hover Background */
color: #ffffff; /* Nav Link Hover Text */
}
.topnav-container nav a.active {
background-color: #3a6fbf; /* Active Nav Link Background (Accent Blue) */
color: #ffffff; /* Active Nav Link Text */
font-weight: bold;
}
/* Common Card Style (base for metric cards, agent cards, etc.) */
.common-card {
background-color: #18223f; /* Card Background: Darker than nav, but lighter than page */
border: 1px solid #2a3f5c; /* Card Border: Accent Border color */
border-radius: 10px;
padding: 1.5rem;
margin-bottom: 1rem; /* Space below cards */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
color: #e0e0e0; /* Default text color within cards */
}
.common-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0,0,0,0.3);
}
.common-card h3 { /* Titles within cards */
color: #b0b0df !important; /* Card Title Color: Slightly lighter than main headings */
font-size: 1.2em !important; /* Slightly larger for card titles */
margin-top: 0 !important; /* Remove default top margin for h3 in card */
margin-bottom: 0.75rem !important;
font-weight: bold !important;
border-bottom: none !important; /* Override general h2 border for card h3 */
}
.common-card p { /* Paragraphs within cards */
font-size: 0.95em !important; /* Slightly smaller for card content */
color: #c0c0dd !important;
margin-bottom: 0.5rem !important;
}
.common-card .icon { /* For icons within cards, like dashboard metric cards */
font-size: 2.5em;
margin-bottom: 0.75rem;
color: #7070ff; /* Icon Color: Muted accent */
}
/* Status Indicators (can be used with <span> or <p> or custom divs) */
.status-indicator {
padding: 0.4rem 0.8rem !important; /* Slightly more padding */
border-radius: 15px !important; /* Pill shape */
font-weight: bold !important;
font-size: 0.85em !important;
display: inline-block !important;
text-align: center !important;
}
.status-success, .status-running { color: #ffffff !important; background-color: #4CAF50 !important; } /* Green */
.status-error { color: #ffffff !important; background-color: #F44336 !important; } /* Red */
.status-warning { color: #000000 !important; background-color: #FFC107 !important; } /* Amber */
.status-info { color: #ffffff !important; background-color: #2196F3 !important; } /* Blue */
.status-stopped { color: #e0e0e0 !important; background-color: #525252 !important; } /* Darker Grey for stopped */
.status-unknown { color: #333333 !important; background-color: #BDBDBD !important; } /* Lighter grey for unknown */
/* Standardized Streamlit Input Styling */
.stTextInput > div > div > input,
.stTextArea > div > div > textarea,
.stSelectbox > div > div,
.stNumberInput > div > div > input {
border: 1px solid #3a3f5c !important; /* Accent Border */
background-color: #1a1f3c !important; /* Nav Background color for inputs */
color: #e0e0e0 !important; /* Default Text Color */
border-radius: 5px !important;
}
.stMultiSelect > div > div > div { /* Multiselect options container */
border: 1px solid #3a3f5c !important;
background-color: #1a1f3c !important;
}
.stMultiSelect span[data-baseweb="tag"] { /* Selected items in multiselect */
background-color: #3a6fbf !important; /* Active Nav Link Background */
}
/* Standardized Streamlit Button Styling */
.stButton > button {
border: 1px solid #3a6fbf !important; /* Accent Blue for border */
background-color: #3a6fbf !important; /* Accent Blue for background */
color: white !important;
border-radius: 5px !important;
padding: 0.5rem 1rem !important;
transition: background-color 0.2s ease, border-color 0.2s ease;
}
.stButton > button:hover {
background-color: #4a7fdc !important; /* Lighter Accent Blue for hover */
border-color: #4a7fdc !important;
}
.stButton > button:disabled {
background-color: #2a2f4c !important;
border-color: #2a2f4c !important;
color: #777777 !important;
}
/* For secondary buttons, Streamlit uses a 'kind' attribute in HTML we can't directly target via pure CSS.
Instead, use st.button(..., type="secondary") and rely on Streamlit's handling,
or use st.markdown for fully custom buttons if default secondary is not enough.
The below attempts to style based on common Streamlit secondary button appearance.
Note: This specific selector for secondary buttons might be unstable if Streamlit changes its internal class names.
*/
.stButton button.st-emotion-cache-LPTKCI { /* Example selector for a secondary button, MAY BE UNSTABLE */
background-color: #2a2f4c !important;
border: 1px solid #2a2f4c !important;
color: #c0c0ff !important;
}
.stButton button.st-emotion-cache-LPTKCI:hover {
background-color: #3a3f5c !important;
border-color: #3a3f5c !important;
color: #ffffff !important;
}
.stButton button.st-emotion-cache-LPTKCI:disabled { /* Disabled secondary button */
background-color: #1e2a47 !important;
border-color: #1e2a47 !important;
color: #555555 !important;
}
/* Dataframe styling */
.stDataFrame { /* Main container for dataframes */
border: 1px solid #2a3f5c !important; /* Accent Border */
border-radius: 5px !important;
background-color: #1a1f3c !important; /* Nav Background for dataframe background */
}
.stDataFrame th { /* Headers */
background-color: #2a2f4c !important; /* Nav Link Hover Background for headers */
color: #d0d0ff !important; /* Primary Heading Color for header text */
font-weight: bold;
}
.stDataFrame td { /* Cells */
color: #c0c0dd !important; /* Softer light text for cell data */
border-bottom-color: #2a3f5c !important; /* Accent border for cell lines */
border-top-color: #2a3f5c !important;
}
</style>
""", unsafe_allow_html=True)
def get_api_status() -> tuple[bool, dict]:
"""
Checks if the backend API is reachable and returns its status.
Uses the `API_BASE_URL` constant defined in this module.
Returns:
tuple[bool, dict]: A tuple where:
- The first element is a boolean: True if the API is reachable and returns a 2xx status, False otherwise.
- The second element is a dictionary:
- If successful, contains API information (e.g., from `response.json()`).
- If unsuccessful, contains an 'error' key with a descriptive message.
"""
try:
response = requests.get(f"{API_BASE_URL}/", timeout=3) # Increased timeout slightly
response.raise_for_status()
return True, response.json()
except requests.exceptions.RequestException as e:
logger.error(f"API connection error in get_api_status (shared_utils): {e}")
return False, {"error": f"API connection failed: {str(e)}"}
except Exception as e:
logger.exception(f"Unexpected error in get_api_status (shared_utils): {e}")
return False, {"error": f"An unexpected error occurred: {str(e)}"}
def get_agent_status() -> Optional[dict]:
"""
Fetches the status for all registered agents from the backend.
Uses the `API_BASE_URL` constant defined in this module.
On successful API call, returns a dictionary where keys are agent IDs
and values are dictionaries containing status and configuration for each agent.
Returns None if the API call fails or an exception occurs.
Returns:
Optional[dict]: Agent statuses dictionary or None.
"""
try:
response = requests.get(f"{API_BASE_URL}/agents/status", timeout=5)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"API error fetching agent status (shared_utils): {e}")
return None
except Exception as e:
logger.exception(f"Unexpected error in get_agent_status (shared_utils): {e}")
return None
def start_agent(agent_id: str) -> dict:
"""
Sends a request to the backend to start a specific agent.
Uses the `API_BASE_URL` constant defined in this module.
Constructs a POST request to the `/agents/{agent_id}/start` endpoint.
Args:
agent_id (str): The unique identifier of the agent to start.
Returns:
dict: A dictionary containing the API response. Typically includes a 'success' boolean
and a 'message' string. In case of connection or unexpected errors,
it also returns a dict with 'success': False and an error 'message'.
"""
try:
response = requests.post(f"{API_BASE_URL}/agents/{agent_id}/start", timeout=7) # Increased timeout
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"API error starting agent {agent_id} (shared_utils): {e}")
return {"success": False, "message": f"Failed to start agent {agent_id}: {str(e)}"}
except Exception as e:
logger.exception(f"Unexpected error in start_agent (shared_utils) for {agent_id}: {e}")
return {"success": False, "message": f"An unexpected error occurred: {str(e)}"}
def stop_agent(agent_id: str) -> dict:
"""
Sends a request to the backend to stop a specific agent.
Uses the `API_BASE_URL` constant defined in this module.
Constructs a POST request to the `/agents/{agent_id}/stop` endpoint.
Args:
agent_id (str): The unique identifier of the agent to stop.
Returns:
dict: A dictionary containing the API response. Typically includes a 'success' boolean
and a 'message' string. In case of connection or unexpected errors,
it also returns a dict with 'success': False and an error 'message'.
"""
try:
response = requests.post(f"{API_BASE_URL}/agents/{agent_id}/stop", timeout=7) # Increased timeout
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"API error stopping agent {agent_id} (shared_utils): {e}")
return {"success": False, "message": f"Failed to stop agent {agent_id}: {str(e)}"}
except Exception as e:
logger.exception(f"Unexpected error in stop_agent (shared_utils) for {agent_id}: {e}")
return {"success": False, "message": f"An unexpected error occurred: {str(e)}"}
def get_datasets() -> list[str]:
"""
Fetches the list of available dataset names from the backend.
Uses the `API_BASE_URL` constant defined in this module.
Targets the `/explorer/datasets` endpoint.
Returns:
list[str]: A list of dataset names. Returns an empty list if the API call
fails, if the 'datasets' key is missing in the response,
or if an exception occurs.
"""
try:
response = requests.get(f"{API_BASE_URL}/explorer/datasets", timeout=5)
response.raise_for_status()
data = response.json()
return data.get("datasets", [])
except requests.exceptions.RequestException as e:
logger.error(f"API error fetching datasets (shared_utils): {e}")
return []
except Exception as e:
logger.exception(f"Unexpected error in get_datasets (shared_utils): {e}")
return []
def get_dataset_preview(dataset_name: str, limit: int = 10) -> Optional[dict]:
"""
Fetches preview data for a specific dataset from the backend.
Uses the `API_BASE_URL` constant defined in this module.
Targets the `/explorer/dataset/{dataset_name}/preview` endpoint with a `limit` parameter.
Args:
dataset_name (str): The name of the dataset to preview.
limit (int): The maximum number of records to fetch for the preview. Defaults to 10.
Returns:
Optional[dict]: A dictionary containing dataset information (e.g., 'dataset',
'record_count', 'preview' list of records) if successful.
Each record in the 'preview' list is a dictionary typically
containing 'id', 'shape', 'dtype', 'metadata', and 'data' (raw list).
Returns None if the API call fails or an exception occurs.
"""
try:
response = requests.get(f"{API_BASE_URL}/explorer/dataset/{dataset_name}/preview?limit={limit}", timeout=10)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"API error fetching dataset preview for {dataset_name} (shared_utils): {e}")
return None
except Exception as e:
logger.exception(f"Unexpected error in get_dataset_preview (shared_utils) for {dataset_name}: {e}")
return None
def get_tensor_metadata(dataset_name: str, tensor_id: str) -> Optional[dict]:
"""Fetch metadata for a specific tensor via the Explorer API."""
try:
response = requests.get(
f"{API_BASE_URL}/explorer/dataset/{dataset_name}/tensor/{tensor_id}/metadata",
timeout=5,
)
response.raise_for_status()
data = response.json()
return data.get("metadata", data)
except requests.exceptions.RequestException as e:
logger.error(
f"API error fetching tensor metadata for {dataset_name}/{tensor_id} (shared_utils): {e}"
)
return None
except Exception as e:
logger.exception(
f"Unexpected error in get_tensor_metadata (shared_utils) for {dataset_name}/{tensor_id}: {e}"
)
return None
def list_all_agents() -> list[dict[str, str]]:
"""
Returns a list of agent details based on the current statuses fetched by `get_agent_status`.
Each agent detail is a dictionary with 'id', 'name', and 'status' keys.
If an agent's name is not explicitly provided in the status data, a default name
is generated by capitalizing the agent_id and replacing underscores with spaces.
This function is a convenience wrapper around `get_agent_status()` if a list
format of agent information is preferred.
Returns:
list[dict[str, str]]: A list of dictionaries, where each dictionary represents an agent
and contains its 'id', 'name', and 'status'.
Returns an empty list if agent statuses cannot be fetched.
"""
agents_status_data = get_agent_status()
if agents_status_data:
return [
{
"id": agent_id,
"name": agent_data.get("name", agent_id.replace("_", " ").title()), # Default formatted name
"status": agent_data.get("status", "unknown")
}
for agent_id, agent_data in agents_status_data.items()
]
return []
def post_nql_query(query: str) -> dict:
"""
Sends an NQL query to the backend for processing.
Uses the `API_BASE_URL` constant defined in this module.
Constructs a POST request to the `/chat/query` endpoint with the user's query.
Args:
query (str): The Natural Query Language query string provided by the user.
Returns:
dict: A dictionary containing the API response.
On success, this typically includes:
- 'query': The original query string.
- 'response_text': A textual summary of the NQL agent's action or findings.
- 'results': A list of records (tensors with metadata) if the query involved data retrieval.
Each record is a dictionary, potentially including 'id', 'shape',
'dtype', 'metadata', and 'data'.
- 'count': Number of results found (if applicable).
On failure (e.g., connection error, API error, unexpected server error):
- 'query': The original query.
- 'response_text': An error message.
- 'error': A more detailed error string.
- 'results': None or an empty list.
"""
try:
response = requests.post(
f"{API_BASE_URL}/chat/query", # Ensure API_BASE_URL is defined in this file
json={"query": query},
timeout=15
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"Error posting NQL query from pages_shared_utils: {e}")
# Let the caller handle UI error display
return {"query": query, "response_text": "Error connecting to backend or processing query.", "error": str(e), "results": None}
except Exception as e:
logger.exception(f"Unexpected error in post_nql_query (pages_shared_utils): {e}")
return {"query": query, "response_text": "An unexpected error occurred.", "error": str(e), "results": None}
# --- Functions to be added from app.py ---
def configure_agent(agent_id: str, config: dict) -> dict:
"""
Sends a request to the backend to configure a specific agent.
Uses the `API_BASE_URL` constant defined in this module.
Constructs a POST request to the `/agents/{agent_id}/configure` endpoint.
Args:
agent_id (str): The unique identifier of the agent to configure.
config (dict): The configuration dictionary for the agent.
Returns:
dict: A dictionary containing the API response. Typically includes 'success' boolean
and a 'message' string. In case of connection or unexpected errors,
it also returns a dict with 'success': False and an error 'message'.
"""
try:
response = requests.post(
f"{API_BASE_URL}/agents/{agent_id}/configure",
json={"config": config},
timeout=7 # Increased timeout similar to start/stop
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"API error configuring agent {agent_id} (shared_utils): {e}")
return {"success": False, "message": f"Failed to configure agent {agent_id}: {str(e)}"}
except Exception as e:
logger.exception(f"Unexpected error in configure_agent (shared_utils) for {agent_id}: {e}")
return {"success": False, "message": f"An unexpected error occurred: {str(e)}"}
def operate_explorer(dataset: str, operation: str, index: int, params: dict) -> dict:
"""
Sends an operation request to the data explorer for a specific tensor.
Uses the `API_BASE_URL` constant defined in this module.
Constructs a POST request to the `/explorer/operate` endpoint.
Args:
dataset (str): The name of the dataset containing the tensor.
operation (str): The operation to perform (e.g., 'view', 'transform').
index (int): The index of the tensor within the dataset.
params (dict): Additional parameters required for the operation.
Returns:
dict: A dictionary containing the API response. Typically includes:
- 'success': A boolean indicating if the operation was accepted.
- 'metadata': A dictionary with details about the operation or resulting tensor.
- 'result_data': The data of the resulting tensor (if applicable), or None.
In case of connection or server-side errors, it returns a dict with
'success': False, 'metadata': {'error': error_message}, and 'result_data': None.
"""
payload = {
"dataset": dataset,
"operation": operation,
"tensor_index": index,
"params": params
}
try:
response = requests.post(
f"{API_BASE_URL}/explorer/operate",
json=payload,
timeout=15 # Standard timeout for potentially long operations
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"API error in operate_explorer for {dataset} (shared_utils): {e}")
return {"success": False, "metadata": {"error": str(e)}, "result_data": None}
except Exception as e:
logger.exception(f"Unexpected error in operate_explorer (shared_utils) for {dataset}: {e}")
return {"success": False, "metadata": {"error": str(e)}, "result_data": None}