1212 streamlit run ui/poc_app.py
1313"""
1414
15+ import io
1516import json
1617import logging
1718import os
19+ import zipfile
1820from pathlib import Path
1921
2022import pandas as pd
@@ -1561,6 +1563,15 @@ def format_display_name(raw_name: str) -> str:
15611563 st .session_state .top5_cost = []
15621564if "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+
24872658def 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