Mark Cuizon commited on
Commit
8f155fd
·
1 Parent(s): c9a0fae

fix-bodystreambuffer-hf

Browse files
.streamlit/config.toml CHANGED
@@ -3,13 +3,20 @@ headless = true
3
  port = 8501
4
  enableCORS = false
5
  enableXsrfProtection = false
 
 
 
6
 
7
  [browser]
8
  gatherUsageStats = false
9
 
 
 
 
 
10
  [theme]
11
  base = "light"
12
  primaryColor = "#4facfe"
13
  backgroundColor = "#ffffff"
14
  secondaryBackgroundColor = "#f0f2f6"
15
- textColor = "#262730"
 
3
  port = 8501
4
  enableCORS = false
5
  enableXsrfProtection = false
6
+ enableWebsocketCompression = false
7
+ maxMessageSize = 700
8
+ maxUploadSize = 200
9
 
10
  [browser]
11
  gatherUsageStats = false
12
 
13
+ [runner]
14
+ fastReruns = false
15
+ enforceSerializableSessionState = false
16
+
17
  [theme]
18
  base = "light"
19
  primaryColor = "#4facfe"
20
  backgroundColor = "#ffffff"
21
  secondaryBackgroundColor = "#f0f2f6"
22
+ textColor = "#262730"
config.toml CHANGED
@@ -3,10 +3,17 @@ headless = true
3
  port = 8501
4
  enableCORS = false
5
  enableXsrfProtection = false
 
 
 
6
 
7
  [browser]
8
  gatherUsageStats = false
9
 
 
 
 
 
10
  [theme]
11
  base = "light"
12
  primaryColor = "#4facfe"
 
3
  port = 8501
4
  enableCORS = false
5
  enableXsrfProtection = false
6
+ enableWebsocketCompression = false
7
+ maxMessageSize = 700
8
+ maxUploadSize = 200
9
 
10
  [browser]
11
  gatherUsageStats = false
12
 
13
+ [runner]
14
+ fastReruns = false
15
+ enforceSerializableSessionState = false
16
+
17
  [theme]
18
  base = "light"
19
  primaryColor = "#4facfe"
modules/results.py CHANGED
@@ -24,6 +24,11 @@ from modules.admin import get_active_prompt
24
  from utils.feedback_system import (
25
  display_feedback_section as display_feedback_section_util)
26
  from utils.translations import t, get_language
 
 
 
 
 
27
 
28
  # Import CropDrive integration for user ID
29
  try:
@@ -665,17 +670,27 @@ def show_results_page():
665
  status_text = st.empty()
666
  time_estimate = st.empty()
667
  step_indicator = st.empty()
 
668
 
669
  # Add a "system is working" indicator
670
  working_indicator = st.empty()
671
 
672
  # Process the new analysis with enhanced progress tracking
 
673
  try:
674
  # Add a note about keeping the page open
675
  st.info(f"⏳ **{t('results_important', 'Important')}:** {t('results_keep_page_open', 'Please keep this page open during analysis. The process may take 5-8 minutes.')}")
676
 
677
  logger.info("🔍 DEBUG - Calling process_new_analysis...")
678
- results_data = process_new_analysis(st.session_state.analysis_data, progress_bar, status_text, time_estimate, step_indicator, working_indicator)
 
 
 
 
 
 
 
 
679
  logger.info(f"🔍 DEBUG - process_new_analysis returned: {type(results_data)}, success={results_data.get('success') if results_data else 'None'}")
680
  except KeyboardInterrupt:
681
  logger.warning("Analysis interrupted by user")
@@ -687,17 +702,31 @@ def show_results_page():
687
  logger.error(f"Traceback: {traceback.format_exc()}")
688
 
689
  # Handle specific error types
690
- if "bodystreambuffer" in error_msg or "aborted" in error_msg:
691
- results_data = {'success': False, 'message': 'Connection was interrupted during analysis. Please refresh the page and try again.'}
 
 
 
 
692
  elif "timeout" in error_msg:
693
  results_data = {'success': False, 'message': 'Analysis timed out. Please try again with smaller files or check your connection.'}
694
  else:
695
  results_data = {'success': False, 'message': f'Processing error: {str(e)}'}
696
  finally:
697
- # Clear the analysis_data from session state after processing (success or failure)
698
- if 'analysis_data' in st.session_state:
 
 
 
 
 
699
  logger.info("🔍 DEBUG - Clearing analysis_data from session state")
700
  del st.session_state.analysis_data
 
 
 
 
 
701
 
702
  if results_data and results_data.get('success', False):
703
  # Clear progress container
@@ -1379,7 +1408,15 @@ def reconstruct_firestore_data(data):
1379
  logger.error(f"❌ Error reconstructing Firestore data: {e}")
1380
  return data
1381
 
1382
- def process_new_analysis(analysis_data, progress_bar, status_text, time_estimate=None, step_indicator=None, working_indicator=None):
 
 
 
 
 
 
 
 
1383
  """Process new analysis data from uploaded files"""
1384
  try:
1385
  import time
@@ -1657,15 +1694,11 @@ def process_new_analysis(analysis_data, progress_bar, status_text, time_estimate
1657
  analysis_engine = AnalysisEngine()
1658
 
1659
  # Analysis processing (optimized - no delays)
1660
- status_text.text("🔬 **Step 4/5:** Running comprehensive agricultural analysis... 🔄")
1661
- time_estimate.text("⏱️ Processing data patterns and generating insights...")
1662
- step_indicator.text(f"📋 Step {current_step} of {total_steps} - AI Analysis")
1663
-
1664
- status_text.text("🔬 **Step 4/5:** Running comprehensive agricultural analysis... ✅")
1665
  if time_estimate:
1666
- time_estimate.text("⏱️ Estimated time remaining: ~15 seconds")
1667
  if step_indicator:
1668
- step_indicator.text(f"📋 Step {current_step} of {total_steps}")
1669
 
1670
  # Transform data structure to match analysis engine expectations
1671
  transformed_soil_samples = []
@@ -1792,13 +1825,26 @@ def process_new_analysis(analysis_data, progress_bar, status_text, time_estimate
1792
  status_text.text("🔬 **Step 4/5:** Starting comprehensive AI analysis... 🔄")
1793
  progress_bar.progress(70)
1794
  if time_estimate:
1795
- time_estimate.text("⏱️ This may take 5-8 minutes. Please keep this page open...")
1796
-
 
 
 
 
 
 
 
 
 
 
 
 
1797
  analysis_results = analysis_engine.generate_comprehensive_analysis(
1798
  soil_data=transformed_soil_data,
1799
  leaf_data=transformed_leaf_data,
1800
  land_yield_data=land_yield_data,
1801
- prompt_text=active_prompt.get('prompt_text', '')
 
1802
  )
1803
  logger.info(f"✅ Analysis completed successfully")
1804
  logger.info(f"🔍 Analysis results keys: {list(analysis_results.keys()) if isinstance(analysis_results, dict) else 'None'}")
@@ -1814,11 +1860,15 @@ def process_new_analysis(analysis_data, progress_bar, status_text, time_estimate
1814
 
1815
  # Check if it's a timeout/connection error
1816
  error_msg = str(e).lower()
1817
- if "timeout" in error_msg or "aborted" in error_msg or "bodystreambuffer" in error_msg or "connection" in error_msg:
1818
- st.error("❌ **Analysis Timeout**: The analysis took too long or the connection was interrupted.")
1819
- st.info("💡 **Tip:** Large files may take longer to process. Please ensure you have a stable internet connection and try again.")
1820
- st.info("💡 **Alternative:** Try refreshing the page and starting the analysis again.")
1821
- return {'success': False, 'message': 'Analysis timeout or connection interrupted'}
 
 
 
 
1822
  elif "quota" in error_msg or "429" in error_msg:
1823
  st.error("❌ **API Quota Exceeded**: The analysis service is temporarily unavailable due to high demand.")
1824
  st.info("💡 **Tip:** Please try again in a few minutes.")
 
24
  from utils.feedback_system import (
25
  display_feedback_section as display_feedback_section_util)
26
  from utils.translations import t, get_language
27
+ from utils.streamlit_runtime import (
28
+ make_progress_callback,
29
+ is_connection_abort_error,
30
+ is_huggingface_space,
31
+ )
32
 
33
  # Import CropDrive integration for user ID
34
  try:
 
670
  status_text = st.empty()
671
  time_estimate = st.empty()
672
  step_indicator = st.empty()
673
+ stream_indicator = st.empty()
674
 
675
  # Add a "system is working" indicator
676
  working_indicator = st.empty()
677
 
678
  # Process the new analysis with enhanced progress tracking
679
+ results_data = None
680
  try:
681
  # Add a note about keeping the page open
682
  st.info(f"⏳ **{t('results_important', 'Important')}:** {t('results_keep_page_open', 'Please keep this page open during analysis. The process may take 5-8 minutes.')}")
683
 
684
  logger.info("🔍 DEBUG - Calling process_new_analysis...")
685
+ results_data = process_new_analysis(
686
+ st.session_state.analysis_data,
687
+ progress_bar,
688
+ status_text,
689
+ time_estimate,
690
+ step_indicator,
691
+ working_indicator,
692
+ stream_indicator,
693
+ )
694
  logger.info(f"🔍 DEBUG - process_new_analysis returned: {type(results_data)}, success={results_data.get('success') if results_data else 'None'}")
695
  except KeyboardInterrupt:
696
  logger.warning("Analysis interrupted by user")
 
702
  logger.error(f"Traceback: {traceback.format_exc()}")
703
 
704
  # Handle specific error types
705
+ if is_connection_abort_error(e):
706
+ results_data = {
707
+ 'success': False,
708
+ 'message': 'Connection was interrupted during analysis. Please refresh the page and try again.',
709
+ 'connection_aborted': True,
710
+ }
711
  elif "timeout" in error_msg:
712
  results_data = {'success': False, 'message': 'Analysis timed out. Please try again with smaller files or check your connection.'}
713
  else:
714
  results_data = {'success': False, 'message': f'Processing error: {str(e)}'}
715
  finally:
716
+ # Keep analysis_data on connection abort so the user can retry without re-uploading
717
+ should_clear_input = (
718
+ results_data
719
+ and results_data.get('success')
720
+ and not results_data.get('connection_aborted')
721
+ )
722
+ if should_clear_input and 'analysis_data' in st.session_state:
723
  logger.info("🔍 DEBUG - Clearing analysis_data from session state")
724
  del st.session_state.analysis_data
725
+ elif results_data and results_data.get('connection_aborted'):
726
+ st.warning(
727
+ "Connection interrupted. Your uploaded files are still saved — "
728
+ "click Refresh, then start analysis again."
729
+ )
730
 
731
  if results_data and results_data.get('success', False):
732
  # Clear progress container
 
1408
  logger.error(f"❌ Error reconstructing Firestore data: {e}")
1409
  return data
1410
 
1411
+ def process_new_analysis(
1412
+ analysis_data,
1413
+ progress_bar,
1414
+ status_text,
1415
+ time_estimate=None,
1416
+ step_indicator=None,
1417
+ working_indicator=None,
1418
+ stream_indicator=None,
1419
+ ):
1420
  """Process new analysis data from uploaded files"""
1421
  try:
1422
  import time
 
1694
  analysis_engine = AnalysisEngine()
1695
 
1696
  # Analysis processing (optimized - no delays)
1697
+ status_text.markdown("🔬 **Step 4/5:** Running comprehensive agricultural analysis... 🔄")
 
 
 
 
1698
  if time_estimate:
1699
+ time_estimate.markdown("⏱️ Processing data patterns and generating insights...")
1700
  if step_indicator:
1701
+ step_indicator.markdown(f"📋 Step {current_step} of {total_steps} - AI Analysis")
1702
 
1703
  # Transform data structure to match analysis engine expectations
1704
  transformed_soil_samples = []
 
1825
  status_text.text("🔬 **Step 4/5:** Starting comprehensive AI analysis... 🔄")
1826
  progress_bar.progress(70)
1827
  if time_estimate:
1828
+ remaining = "3-6 minutes" if is_huggingface_space() else "5-8 minutes"
1829
+ time_estimate.markdown(
1830
+ f"⏱️ This may take {remaining}. Please keep this page open..."
1831
+ )
1832
+
1833
+ progress_callback = make_progress_callback(
1834
+ progress_bar=progress_bar,
1835
+ status_text=status_text,
1836
+ time_estimate=time_estimate,
1837
+ step_indicator=step_indicator,
1838
+ working_indicator=working_indicator,
1839
+ stream_indicator=stream_indicator,
1840
+ )
1841
+
1842
  analysis_results = analysis_engine.generate_comprehensive_analysis(
1843
  soil_data=transformed_soil_data,
1844
  leaf_data=transformed_leaf_data,
1845
  land_yield_data=land_yield_data,
1846
+ prompt_text=active_prompt.get('prompt_text', ''),
1847
+ progress_callback=progress_callback,
1848
  )
1849
  logger.info(f"✅ Analysis completed successfully")
1850
  logger.info(f"🔍 Analysis results keys: {list(analysis_results.keys()) if isinstance(analysis_results, dict) else 'None'}")
 
1860
 
1861
  # Check if it's a timeout/connection error
1862
  error_msg = str(e).lower()
1863
+ if is_connection_abort_error(e) or "timeout" in error_msg:
1864
+ st.error("❌ **Connection Interrupted**: The analysis stream was aborted before completion.")
1865
+ st.info("💡 **Tip:** Keep this browser tab open and retry. On Hugging Face, long AI runs now stream progress to prevent disconnects.")
1866
+ st.info("💡 **Alternative:** Refresh the page and run the analysis again — your uploaded files are still saved.")
1867
+ return {
1868
+ 'success': False,
1869
+ 'message': 'Analysis timeout or connection interrupted',
1870
+ 'connection_aborted': True,
1871
+ }
1872
  elif "quota" in error_msg or "429" in error_msg:
1873
  st.error("❌ **API Quota Exceeded**: The analysis service is temporarily unavailable due to high demand.")
1874
  st.info("💡 **Tip:** Please try again in a few minutes.")
utils/analysis_engine.py CHANGED
@@ -2592,8 +2592,11 @@ class PromptAnalyzer:
2592
  # Use optimal settings from memory: temperature=0.0 for maximum accuracy [[memory:7795938]]
2593
  temperature = 0.0
2594
 
2595
- # Use maximum available tokens for Gemini 2.5 Pro [[memory:7795941]]
2596
- max_tokens = 65536 # Gemini 2.5 Pro maximum output tokens
 
 
 
2597
 
2598
  # Ensure the API key is available to all client layers
2599
  try:
@@ -2710,7 +2713,8 @@ class PromptAnalyzer:
2710
  def generate_step_analysis(self, step: Dict[str, str], soil_params: Dict[str, Any],
2711
  leaf_params: Dict[str, Any], land_yield_data: Dict[str, Any],
2712
  previous_results: List[Dict[str, Any]] = None, total_steps: int = None,
2713
- runtime_ctx: Dict[str, Any] = None) -> Dict[str, Any]:
 
2714
  """Generate analysis for a specific step using LLM"""
2715
  try:
2716
  # Ensure LLM is available before proceeding
@@ -3119,45 +3123,78 @@ class PromptAnalyzer:
3119
 
3120
  Please provide your analysis in the requested JSON format. Be specific and detailed in your findings and recommendations. Use the research references to support your analysis where relevant."""
3121
 
 
 
 
 
 
 
 
 
 
 
3122
  # Generate response using Google Gemini with retries
3123
  self.logger.info(f"Generating LLM response for Step {step['number']}")
3124
  last_err = None
3125
  for attempt in range(1, (getattr(self.ai_config, 'retry_attempts', 3) or 3) + 1):
3126
  try:
3127
  if hasattr(self, '_use_direct_gemini') and self._use_direct_gemini:
3128
- # Use direct Gemini API
3129
  import google.generativeai as genai
3130
  combined_prompt = f"{system_prompt}\n\n{human_prompt}"
3131
  generation_config = genai.types.GenerationConfig(
3132
  temperature=self._temperature,
3133
  max_output_tokens=self._max_tokens,
3134
  )
3135
- resp_obj = self.llm.generate_content(
 
3136
  combined_prompt,
3137
  generation_config=generation_config,
3138
- safety_settings=getattr(self, '_safety_settings', None)
 
3139
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
3140
  class GeminiResponse:
3141
  def __init__(self, content):
3142
  self.content = content
3143
-
3144
- # Check if response is valid
3145
- if not resp_obj.candidates or len(resp_obj.candidates) == 0:
3146
- raise Exception(f"No response candidates generated. Safety filters may have blocked content.")
3147
-
3148
- candidate = resp_obj.candidates[0]
3149
- if hasattr(candidate, 'finish_reason') and candidate.finish_reason != 1: # 1 = STOP (successful completion)
3150
- finish_reason_names = {0: "UNSPECIFIED", 1: "STOP", 2: "MAX_TOKENS", 3: "SAFETY", 4: "RECITATION", 5: "OTHER"}
3151
- reason_name = finish_reason_names.get(candidate.finish_reason, f"UNKNOWN_{candidate.finish_reason}")
3152
- raise Exception(f"Response generation failed with finish_reason: {reason_name} ({candidate.finish_reason}). This may be due to safety filters or content policy violations.")
3153
-
3154
- if not hasattr(resp_obj, 'text') or not resp_obj.text:
3155
  raise Exception("Empty response from Gemini API. This may be due to safety filters.")
3156
-
3157
- response = GeminiResponse(resp_obj.text)
3158
  else:
3159
- # Use LangChain client
3160
- response = self.llm.invoke(system_prompt + "\n\n" + human_prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3161
  last_err = None
3162
  break
3163
  except Exception as e:
@@ -7955,12 +7992,20 @@ class AnalysisEngine:
7955
  return {}
7956
 
7957
  def generate_comprehensive_analysis(self, soil_data: Dict[str, Any], leaf_data: Dict[str, Any],
7958
- land_yield_data: Dict[str, Any], prompt_text: str) -> Dict[str, Any]:
 
7959
  """Generate comprehensive analysis with all components (enhanced)"""
7960
  try:
7961
  self.logger.info("Starting enhanced comprehensive analysis")
7962
  start_time = datetime.now()
7963
 
 
 
 
 
 
 
 
7964
  # Initialize previous_results for comprehensive analysis (no prior steps)
7965
  previous_results = []
7966
 
@@ -7987,6 +8032,8 @@ class AnalysisEngine:
7987
  else:
7988
  self.logger.info("No structured leaf data found in session state")
7989
 
 
 
7990
  # Step 1: Preprocess raw data (skip if already processed)
7991
  self.logger.info("Preprocessing data...")
7992
  if not (soil_data and 'parameter_statistics' in soil_data):
@@ -8075,6 +8122,8 @@ class AnalysisEngine:
8075
  self.logger.info("Generating economic forecast...")
8076
  economic_forecast = self.results_generator.generate_economic_forecast(land_yield_data, recommendations, previous_results)
8077
 
 
 
8078
  # Step 6: Process prompt steps with LLM (enhanced)
8079
  self.logger.info("Processing analysis steps...")
8080
  steps = self.prompt_analyzer.extract_steps_from_prompt(prompt_text)
@@ -8091,11 +8140,23 @@ class AnalysisEngine:
8091
  # Inject runtime context for real-time, seasonal adjustments
8092
  runtime_ctx = self._get_runtime_context()
8093
  step_result = self.prompt_analyzer.generate_step_analysis(
8094
- step, soil_params, leaf_params, land_yield_data, step_results, len(steps), runtime_ctx
 
 
 
 
 
 
 
8095
  )
8096
  # Normalize structure (remove item_0 keys, parse inner JSON, drop raw dumps)
8097
  step_result = self._normalize_step_result(step_result)
8098
  step_results.append(step_result)
 
 
 
 
 
8099
  except Exception as step_error:
8100
  self.logger.error(f"Error processing step {step.get('number', 'unknown')}: {str(step_error)}")
8101
  # Add fallback step result
 
2592
  # Use optimal settings from memory: temperature=0.0 for maximum accuracy [[memory:7795938]]
2593
  temperature = 0.0
2594
 
2595
+ # HF Spaces: smaller outputs finish faster and reduce websocket timeouts
2596
+ if os.getenv("SPACE_ID") or os.getenv("HF_HOME"):
2597
+ max_tokens = 16384
2598
+ else:
2599
+ max_tokens = 65536
2600
 
2601
  # Ensure the API key is available to all client layers
2602
  try:
 
2713
  def generate_step_analysis(self, step: Dict[str, str], soil_params: Dict[str, Any],
2714
  leaf_params: Dict[str, Any], land_yield_data: Dict[str, Any],
2715
  previous_results: List[Dict[str, Any]] = None, total_steps: int = None,
2716
+ runtime_ctx: Dict[str, Any] = None,
2717
+ progress_callback: Any = None) -> Dict[str, Any]:
2718
  """Generate analysis for a specific step using LLM"""
2719
  try:
2720
  # Ensure LLM is available before proceeding
 
3123
 
3124
  Please provide your analysis in the requested JSON format. Be specific and detailed in your findings and recommendations. Use the research references to support your analysis where relevant."""
3125
 
3126
+ if progress_callback and hasattr(progress_callback, 'on_step_start'):
3127
+ try:
3128
+ progress_callback.on_step_start(
3129
+ int(step['number']),
3130
+ step.get('title', f"Step {step['number']}"),
3131
+ total_steps or 1,
3132
+ )
3133
+ except Exception:
3134
+ pass
3135
+
3136
  # Generate response using Google Gemini with retries
3137
  self.logger.info(f"Generating LLM response for Step {step['number']}")
3138
  last_err = None
3139
  for attempt in range(1, (getattr(self.ai_config, 'retry_attempts', 3) or 3) + 1):
3140
  try:
3141
  if hasattr(self, '_use_direct_gemini') and self._use_direct_gemini:
3142
+ # Use direct Gemini API with streaming to keep HF websocket alive
3143
  import google.generativeai as genai
3144
  combined_prompt = f"{system_prompt}\n\n{human_prompt}"
3145
  generation_config = genai.types.GenerationConfig(
3146
  temperature=self._temperature,
3147
  max_output_tokens=self._max_tokens,
3148
  )
3149
+ response_text = ""
3150
+ stream = self.llm.generate_content(
3151
  combined_prompt,
3152
  generation_config=generation_config,
3153
+ safety_settings=getattr(self, '_safety_settings', None),
3154
+ stream=True,
3155
  )
3156
+ for chunk in stream:
3157
+ chunk_text = getattr(chunk, "text", None) or ""
3158
+ if chunk_text:
3159
+ response_text += chunk_text
3160
+ if progress_callback and hasattr(progress_callback, 'on_stream_chunk'):
3161
+ try:
3162
+ progress_callback.on_stream_chunk(
3163
+ int(step['number']),
3164
+ len(response_text),
3165
+ )
3166
+ except Exception:
3167
+ pass
3168
+
3169
  class GeminiResponse:
3170
  def __init__(self, content):
3171
  self.content = content
3172
+
3173
+ if not response_text.strip():
 
 
 
 
 
 
 
 
 
 
3174
  raise Exception("Empty response from Gemini API. This may be due to safety filters.")
3175
+
3176
+ response = GeminiResponse(response_text)
3177
  else:
3178
+ # Use LangChain client with streaming when available
3179
+ response_text = ""
3180
+ if hasattr(self.llm, "stream"):
3181
+ for chunk in self.llm.stream(system_prompt + "\n\n" + human_prompt):
3182
+ chunk_text = getattr(chunk, "content", None) or str(chunk)
3183
+ response_text += chunk_text
3184
+ if progress_callback and hasattr(progress_callback, 'on_stream_chunk'):
3185
+ try:
3186
+ progress_callback.on_stream_chunk(
3187
+ int(step['number']),
3188
+ len(response_text),
3189
+ )
3190
+ except Exception:
3191
+ pass
3192
+ class LangChainResponse:
3193
+ def __init__(self, content):
3194
+ self.content = content
3195
+ response = LangChainResponse(response_text)
3196
+ else:
3197
+ response = self.llm.invoke(system_prompt + "\n\n" + human_prompt)
3198
  last_err = None
3199
  break
3200
  except Exception as e:
 
7992
  return {}
7993
 
7994
  def generate_comprehensive_analysis(self, soil_data: Dict[str, Any], leaf_data: Dict[str, Any],
7995
+ land_yield_data: Dict[str, Any], prompt_text: str,
7996
+ progress_callback: Any = None) -> Dict[str, Any]:
7997
  """Generate comprehensive analysis with all components (enhanced)"""
7998
  try:
7999
  self.logger.info("Starting enhanced comprehensive analysis")
8000
  start_time = datetime.now()
8001
 
8002
+ def _report_phase(message: str, fraction: float) -> None:
8003
+ if progress_callback and hasattr(progress_callback, 'on_phase'):
8004
+ try:
8005
+ progress_callback.on_phase(message, fraction)
8006
+ except Exception:
8007
+ pass
8008
+
8009
  # Initialize previous_results for comprehensive analysis (no prior steps)
8010
  previous_results = []
8011
 
 
8032
  else:
8033
  self.logger.info("No structured leaf data found in session state")
8034
 
8035
+ _report_phase("🧮 Preprocessing soil and leaf data...", 0.05)
8036
+
8037
  # Step 1: Preprocess raw data (skip if already processed)
8038
  self.logger.info("Preprocessing data...")
8039
  if not (soil_data and 'parameter_statistics' in soil_data):
 
8122
  self.logger.info("Generating economic forecast...")
8123
  economic_forecast = self.results_generator.generate_economic_forecast(land_yield_data, recommendations, previous_results)
8124
 
8125
+ _report_phase("📊 Comparing results against MPOB standards...", 0.2)
8126
+
8127
  # Step 6: Process prompt steps with LLM (enhanced)
8128
  self.logger.info("Processing analysis steps...")
8129
  steps = self.prompt_analyzer.extract_steps_from_prompt(prompt_text)
 
8140
  # Inject runtime context for real-time, seasonal adjustments
8141
  runtime_ctx = self._get_runtime_context()
8142
  step_result = self.prompt_analyzer.generate_step_analysis(
8143
+ step,
8144
+ soil_params,
8145
+ leaf_params,
8146
+ land_yield_data,
8147
+ step_results,
8148
+ len(steps),
8149
+ runtime_ctx,
8150
+ progress_callback=progress_callback,
8151
  )
8152
  # Normalize structure (remove item_0 keys, parse inner JSON, drop raw dumps)
8153
  step_result = self._normalize_step_result(step_result)
8154
  step_results.append(step_result)
8155
+ if progress_callback and hasattr(progress_callback, 'on_step_complete'):
8156
+ try:
8157
+ progress_callback.on_step_complete(int(step.get('number', len(step_results))))
8158
+ except Exception:
8159
+ pass
8160
  except Exception as step_error:
8161
  self.logger.error(f"Error processing step {step.get('number', 'unknown')}: {str(step_error)}")
8162
  # Add fallback step result
utils/streamlit_runtime.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Streamlit runtime helpers for long-running jobs on Hugging Face Spaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from typing import Any, Callable, Optional
8
+
9
+
10
+ def is_huggingface_space() -> bool:
11
+ """Detect Hugging Face Spaces runtime."""
12
+ return bool(
13
+ os.getenv("SPACE_ID")
14
+ or os.getenv("HF_HOME")
15
+ or os.getenv("SYSTEM")
16
+ )
17
+
18
+
19
+ class AnalysisProgressReporter:
20
+ """Keeps the Streamlit websocket alive during long Gemini calls."""
21
+
22
+ def __init__(
23
+ self,
24
+ progress_bar: Any = None,
25
+ status_text: Any = None,
26
+ time_estimate: Any = None,
27
+ step_indicator: Any = None,
28
+ working_indicator: Any = None,
29
+ stream_indicator: Any = None,
30
+ base_progress: float = 70.0,
31
+ progress_span: float = 25.0,
32
+ ):
33
+ self.progress_bar = progress_bar
34
+ self.status_text = status_text
35
+ self.time_estimate = time_estimate
36
+ self.step_indicator = step_indicator
37
+ self.working_indicator = working_indicator
38
+ self.stream_indicator = stream_indicator
39
+ self.base_progress = base_progress
40
+ self.progress_span = progress_span
41
+ self._last_flush = 0.0
42
+ self._total_steps = 1
43
+
44
+ def on_phase(self, message: str, fraction: float = 0.0) -> None:
45
+ self._update(
46
+ progress=self.base_progress + (self.progress_span * fraction),
47
+ status=message,
48
+ working=message,
49
+ )
50
+
51
+ def on_step_start(self, step_number: int, step_title: str, total_steps: int) -> None:
52
+ self._total_steps = max(total_steps, 1)
53
+ fraction = max(0.0, (step_number - 1) / self._total_steps)
54
+ self.on_phase(
55
+ f"🤖 **AI Step {step_number}/{total_steps}:** {step_title}...",
56
+ fraction=fraction,
57
+ )
58
+
59
+ def on_stream_chunk(self, step_number: int, chars_received: int) -> None:
60
+ now = time.time()
61
+ if now - self._last_flush < 0.75:
62
+ return
63
+ self._last_flush = now
64
+ fraction = min(0.99, ((step_number - 1) + min(chars_received / 4000.0, 0.95)) / self._total_steps)
65
+ progress = int(self.base_progress + (self.progress_span * fraction))
66
+ stream_msg = f"Receiving AI output... {chars_received:,} characters"
67
+ self._update(
68
+ progress=progress,
69
+ status=f"🤖 **AI Step {step_number}/{self._total_steps}:** {stream_msg}",
70
+ working=stream_msg,
71
+ stream=stream_msg,
72
+ )
73
+
74
+ def on_step_complete(self, step_number: int) -> None:
75
+ fraction = min(1.0, step_number / self._total_steps)
76
+ self.on_phase(
77
+ f"✅ **AI Step {step_number}/{self._total_steps}** complete",
78
+ fraction=fraction,
79
+ )
80
+
81
+ def _update(
82
+ self,
83
+ progress: float,
84
+ status: str,
85
+ working: Optional[str] = None,
86
+ stream: Optional[str] = None,
87
+ ) -> None:
88
+ try:
89
+ if self.progress_bar is not None:
90
+ self.progress_bar.progress(min(max(int(progress), 0), 100))
91
+ if self.status_text is not None:
92
+ self.status_text.markdown(status)
93
+ if self.working_indicator is not None and working:
94
+ self.working_indicator.markdown(f"🔄 **Processing:** {working}")
95
+ if self.stream_indicator is not None and stream:
96
+ self.stream_indicator.caption(stream)
97
+ if self.time_estimate is not None:
98
+ self.time_estimate.markdown(
99
+ "⏱️ Analysis is still running — keep this tab open."
100
+ )
101
+ if self.step_indicator is not None:
102
+ self.step_indicator.markdown("📋 AI analysis in progress")
103
+ except Exception:
104
+ pass
105
+
106
+
107
+ def make_progress_callback(
108
+ progress_bar: Any = None,
109
+ status_text: Any = None,
110
+ time_estimate: Any = None,
111
+ step_indicator: Any = None,
112
+ working_indicator: Any = None,
113
+ stream_indicator: Any = None,
114
+ ) -> AnalysisProgressReporter:
115
+ return AnalysisProgressReporter(
116
+ progress_bar=progress_bar,
117
+ status_text=status_text,
118
+ time_estimate=time_estimate,
119
+ step_indicator=step_indicator,
120
+ working_indicator=working_indicator,
121
+ stream_indicator=stream_indicator,
122
+ )
123
+
124
+
125
+ def is_connection_abort_error(error: Exception | str) -> bool:
126
+ message = str(error).lower()
127
+ return any(
128
+ token in message
129
+ for token in (
130
+ "bodystreambuffer",
131
+ "aborted",
132
+ "connection reset",
133
+ "connection interrupted",
134
+ "client disconnected",
135
+ "stream closed",
136
+ )
137
+ )