Skip to content

Commit db93407

Browse files
authored
Merge pull request #69 from anfredette/deployment
feat: Support selecting an option and generating deployment yaml files
2 parents 8a2cb90 + 2912d06 commit db93407

File tree

1 file changed

+241
-7
lines changed

1 file changed

+241
-7
lines changed

ui/app.py

Lines changed: 241 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
streamlit run ui/poc_app.py
1313
"""
1414

15+
import io
1516
import json
1617
import logging
1718
import os
19+
import zipfile
1820
from pathlib import Path
1921

2022
import pandas as pd
@@ -1561,6 +1563,15 @@ def format_display_name(raw_name: str) -> str:
15611563
st.session_state.top5_cost = []
15621564
if "top5_simplest" not in st.session_state:
15631565
st.session_state.top5_simplest = []
1566+
# Deployment tab state
1567+
if "deployment_selected_config" not in st.session_state:
1568+
st.session_state.deployment_selected_config = None
1569+
if "deployment_yaml_files" not in st.session_state:
1570+
st.session_state.deployment_yaml_files = {}
1571+
if "deployment_id" not in st.session_state:
1572+
st.session_state.deployment_id = None
1573+
if "deployment_yaml_generated" not in st.session_state:
1574+
st.session_state.deployment_yaml_generated = False
15641575

15651576
# =============================================================================
15661577
# HELPER FUNCTIONS
@@ -2484,6 +2495,166 @@ def render_hero():
24842495
""", unsafe_allow_html=True)
24852496

24862497

2498+
def render_deployment_tab():
2499+
"""Tab 4: Deployment configuration and YAML generation."""
2500+
st.markdown('<h3 style="color: white; font-weight: 700;">Deployment Configuration</h3>', unsafe_allow_html=True)
2501+
2502+
# Check if a configuration is selected
2503+
selected_config = st.session_state.get("deployment_selected_config")
2504+
2505+
if not selected_config:
2506+
st.info("No configuration selected for deployment. Please select a configuration from the Recommendations tab.")
2507+
st.markdown("""
2508+
**How to select a configuration:**
2509+
1. Go to the **Recommendations** tab
2510+
2. Browse the available configurations
2511+
3. Click **Select for Deployment** on your preferred configuration
2512+
""")
2513+
return
2514+
2515+
# Display selected configuration summary
2516+
model_name = selected_config.get("model_name", "Unknown Model")
2517+
gpu_config = selected_config.get("gpu_config", {})
2518+
gpu_type = gpu_config.get("gpu_type", "Unknown")
2519+
gpu_count = gpu_config.get("gpu_count", 1)
2520+
replicas = gpu_config.get("replicas", 1)
2521+
2522+
st.markdown(f"""
2523+
<div style="background: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(34, 197, 94, 0.05));
2524+
border: 1px solid rgba(34, 197, 94, 0.3); border-radius: 12px; padding: 1rem; margin-bottom: 1.5rem;">
2525+
<div style="display: flex; align-items: center; gap: 2rem; flex-wrap: wrap;">
2526+
<div>
2527+
<span style="color: rgba(255,255,255,0.6); font-size: 0.85rem;">Model:</span>
2528+
<span style="color: white; font-weight: 600; font-size: 1rem; margin-left: 0.5rem;">{model_name}</span>
2529+
</div>
2530+
<div>
2531+
<span style="color: rgba(255,255,255,0.6); font-size: 0.85rem;">GPU:</span>
2532+
<span style="color: white; font-weight: 600; font-size: 1rem; margin-left: 0.5rem;">{gpu_count}x{gpu_type}</span>
2533+
</div>
2534+
<div>
2535+
<span style="color: rgba(255,255,255,0.6); font-size: 0.85rem;">Replicas:</span>
2536+
<span style="color: white; font-weight: 600; font-size: 1rem; margin-left: 0.5rem;">{replicas}</span>
2537+
</div>
2538+
</div>
2539+
</div>
2540+
""", unsafe_allow_html=True)
2541+
2542+
# YAML Generation Section
2543+
if not st.session_state.get("deployment_yaml_generated"):
2544+
# YAML files not yet generated (edge case - shouldn't normally happen with auto-generation)
2545+
st.markdown('<h3 style="color: white; font-weight: 700;">Deployment Files</h3>', unsafe_allow_html=True)
2546+
st.warning("YAML files have not been generated yet.")
2547+
2548+
if st.button("Generate YAML Files", type="primary", key="generate_yaml_btn"):
2549+
with st.spinner("Generating deployment files..."):
2550+
try:
2551+
# Call backend API to generate YAML files
2552+
response = requests.post(
2553+
f"{API_BASE_URL}/api/deploy",
2554+
json={
2555+
"recommendation": selected_config,
2556+
"namespace": "default"
2557+
},
2558+
timeout=30
2559+
)
2560+
response.raise_for_status()
2561+
result = response.json()
2562+
2563+
if result.get("success"):
2564+
deployment_id = result.get("deployment_id")
2565+
st.session_state.deployment_id = deployment_id
2566+
2567+
# Fetch the actual YAML content
2568+
yaml_response = requests.get(
2569+
f"{API_BASE_URL}/api/deployments/{deployment_id}/yaml",
2570+
timeout=10
2571+
)
2572+
yaml_response.raise_for_status()
2573+
yaml_data = yaml_response.json()
2574+
2575+
st.session_state.deployment_yaml_files = yaml_data.get("files", {})
2576+
st.session_state.deployment_yaml_generated = True
2577+
st.rerun()
2578+
else:
2579+
st.session_state.deployment_error = result.get('message', 'Unknown error')
2580+
st.rerun()
2581+
2582+
except requests.exceptions.RequestException as e:
2583+
st.session_state.deployment_error = f"Failed to connect to backend API: {e}"
2584+
st.rerun()
2585+
except Exception as e:
2586+
st.session_state.deployment_error = f"Error generating YAML files: {e}"
2587+
st.rerun()
2588+
else:
2589+
# Display generated YAML files
2590+
deployment_id = st.session_state.get("deployment_id", "unknown")
2591+
yaml_files = st.session_state.get("deployment_yaml_files", {})
2592+
deployment_error = st.session_state.get("deployment_error")
2593+
2594+
# Prepare zip file for download
2595+
zip_buffer = None
2596+
if yaml_files:
2597+
zip_buffer = io.BytesIO()
2598+
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
2599+
for filename, content in yaml_files.items():
2600+
zf.writestr(filename, content)
2601+
zip_buffer.seek(0)
2602+
2603+
# Header with download icon and ID/error status
2604+
header_col1, header_col2 = st.columns([0.07, 0.93])
2605+
with header_col1:
2606+
if zip_buffer:
2607+
st.download_button(
2608+
label="⬇",
2609+
data=zip_buffer.getvalue(),
2610+
file_name=f"{deployment_id}.zip",
2611+
mime="application/zip",
2612+
key="download_all_yaml",
2613+
help="Download all YAML files as zip"
2614+
)
2615+
else:
2616+
st.markdown("⬇", unsafe_allow_html=True)
2617+
with header_col2:
2618+
if deployment_error:
2619+
st.markdown(f'<h3 style="color: white; font-weight: 700; margin: 0;">Deployment Files <span style="color: #EE0000; font-weight: 700;">(ERROR!)</span></h3>', unsafe_allow_html=True)
2620+
st.error(deployment_error)
2621+
else:
2622+
st.markdown(f'<h3 style="color: white; font-weight: 700; margin: 0;">Deployment Files <span style="color: rgba(255,255,255,0.5); font-weight: 400; font-size: 0.8rem;">({deployment_id})</span></h3>', unsafe_allow_html=True)
2623+
2624+
if yaml_files:
2625+
# Sort files for consistent display order
2626+
file_order = ["inferenceservice", "autoscaling", "servicemonitor", "vllm-config"]
2627+
file_labels = {
2628+
"inferenceservice": "InferenceService (KServe)",
2629+
"autoscaling": "Autoscaling (HPA)",
2630+
"servicemonitor": "ServiceMonitor (Prometheus)",
2631+
"vllm-config": "vLLM Config (Reference)"
2632+
}
2633+
2634+
for file_key in file_order:
2635+
# Find matching file
2636+
matching_file = None
2637+
matching_content = None
2638+
for filename, content in yaml_files.items():
2639+
if file_key in filename.lower():
2640+
matching_file = filename
2641+
matching_content = content
2642+
break
2643+
2644+
if matching_content:
2645+
label = file_labels.get(file_key, file_key)
2646+
with st.expander(f"{label}", expanded=False):
2647+
st.code(matching_content, language="yaml")
2648+
2649+
# Regenerate button
2650+
if st.button("Regenerate YAML Files", key="regenerate_yaml_btn"):
2651+
st.session_state.deployment_yaml_generated = False
2652+
st.session_state.deployment_yaml_files = {}
2653+
st.session_state.deployment_id = None
2654+
st.session_state.deployment_error = None
2655+
st.rerun()
2656+
2657+
24872658
def render_about_section(models_df: pd.DataFrame):
24882659
"""Render About section at the bottom with expandable info."""
24892660
st.markdown("""
@@ -3020,8 +3191,8 @@ def render_carousel_card(title, color, recs_list, highlight_field, category_key,
30203191
</div>
30213192
</div>'''
30223193
st.markdown(card_html, unsafe_allow_html=True)
3023-
3024-
# Small arrow buttons inside card footer area
3194+
3195+
# Small arrow buttons and Select for Deployment button
30253196
btn_col1, btn_col2, btn_col3 = st.columns([1, 2, 1])
30263197
with btn_col1:
30273198
if st.button("◀", key=f"prev_{category_key}", use_container_width=True):
@@ -3031,6 +3202,66 @@ def render_carousel_card(title, color, recs_list, highlight_field, category_key,
30313202
st.session_state.show_winner_dialog = False
30323203
# Use session state directly to get latest value
30333204
st.session_state[idx_key] = (st.session_state[idx_key] - 1) % total
3205+
with btn_col2:
3206+
# Check if THIS SPECIFIC CARD is selected (by category_key, not just model/GPU)
3207+
selected_category = st.session_state.get("deployment_selected_category")
3208+
is_selected = (selected_category == category_key)
3209+
3210+
if is_selected:
3211+
# Show green "Selected" button with checkmark - clicking deselects
3212+
if st.button("✓ Selected", key=f"selected_{category_key}", use_container_width=True,
3213+
type="primary", help="Click to deselect this configuration"):
3214+
# Clear the selection
3215+
st.session_state.deployment_selected_config = None
3216+
st.session_state.deployment_selected_category = None
3217+
st.session_state.deployment_yaml_generated = False
3218+
st.session_state.deployment_yaml_files = {}
3219+
st.session_state.deployment_id = None
3220+
st.session_state.deployment_error = None
3221+
st.rerun()
3222+
else:
3223+
# Show "Select" button - clicking selects
3224+
if st.button("Select", key=f"select_{category_key}", use_container_width=True,
3225+
help="Select this configuration for deployment"):
3226+
# Store the current recommendation and which card it came from
3227+
st.session_state.deployment_selected_config = rec
3228+
st.session_state.deployment_selected_category = category_key
3229+
st.session_state.deployment_yaml_generated = False
3230+
st.session_state.deployment_yaml_files = {}
3231+
st.session_state.deployment_id = None
3232+
3233+
# Auto-generate YAML files
3234+
try:
3235+
response = requests.post(
3236+
f"{API_BASE_URL}/api/deploy",
3237+
json={
3238+
"recommendation": rec,
3239+
"namespace": "default"
3240+
},
3241+
timeout=30
3242+
)
3243+
response.raise_for_status()
3244+
result = response.json()
3245+
3246+
if result.get("success"):
3247+
deployment_id = result.get("deployment_id")
3248+
st.session_state.deployment_id = deployment_id
3249+
3250+
# Fetch the actual YAML content
3251+
yaml_response = requests.get(
3252+
f"{API_BASE_URL}/api/deployments/{deployment_id}/yaml",
3253+
timeout=10
3254+
)
3255+
yaml_response.raise_for_status()
3256+
yaml_data = yaml_response.json()
3257+
3258+
st.session_state.deployment_yaml_files = yaml_data.get("files", {})
3259+
st.session_state.deployment_yaml_generated = True
3260+
else:
3261+
st.session_state.deployment_yaml_generated = False
3262+
except Exception:
3263+
st.session_state.deployment_yaml_generated = False
3264+
st.rerun()
30343265
with btn_col3:
30353266
if st.button("▶", key=f"next_{category_key}", use_container_width=True):
30363267
# IMPORTANT: Reset dialog states to prevent table from opening
@@ -4202,19 +4433,22 @@ def main():
42024433
# Main Content - Compact hero
42034434
render_hero()
42044435

4205-
# Tab-based navigation (4 tabs)
4206-
tab1, tab2, tab3, tab4 = st.tabs(["Define Use Case", "Technical Specification", "Recommendations", "About"])
4207-
4436+
# Tab-based navigation (5 tabs)
4437+
tab1, tab2, tab3, tab4, tab5 = st.tabs(["Define Use Case", "Technical Specification", "Recommendations", "Deployment", "About"])
4438+
42084439
with tab1:
42094440
render_use_case_input_tab(priority, models_df)
4210-
4441+
42114442
with tab2:
42124443
render_technical_specs_tab(priority, models_df)
4213-
4444+
42144445
with tab3:
42154446
render_results_tab(priority, models_df)
42164447

42174448
with tab4:
4449+
render_deployment_tab()
4450+
4451+
with tab5:
42184452
render_about_section(models_df)
42194453

42204454
# Handle tab switching after approval

0 commit comments

Comments
 (0)