broadfield-dev commited on
Commit
8b73dfb
·
verified ·
1 Parent(s): 0e640ed

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -45
app.py CHANGED
@@ -358,7 +358,7 @@ Select one action and its input. Output JSON only."""
358
  logger.warning(f"PUI_GRADIO [{request_id}]: Missing input for {action_type}. Falling back.")
359
  action_type = "quick_respond" # Fallback to quick_respond logic above
360
  final_system_prompt_str += " Respond directly. (Note: A web action was attempted but failed due to missing input)."
361
- final_user_prompt_content_str = f"History:\n{history_str_for_prompt}\nGuidelines:\n{initial_insights_ctx_str}\nQuery: \"{user_input}\"\nResponse:"
362
  else:
363
  yield "status", f"<i>[Web: '{query_or_url[:60]}'...]</i>"
364
  web_results_data = []
@@ -387,12 +387,12 @@ Select one action and its input. Output JSON only."""
387
 
388
  yield "status", "<i>[Synthesizing web report...]</i>"
389
  final_system_prompt_str += " Generate a report/answer from web content, history, & guidelines. Cite URLs as [Source X]."
390
- final_user_prompt_content_str = f"History:\n{history_str_for_prompt}\nGuidelines:\n{initial_insights_ctx_str}\n\nWeb Content Found:\n{scraped_content_for_prompt}\n\nUser's Query: \"{user_input}\"\n\nYour report/response (cite sources like [Source 1], [Source 2]):"
391
 
392
  else: # Fallback if action_type is somehow unknown
393
  logger.warning(f"PUI_GRADIO [{request_id}]: Unknown action_type '{action_type}'. Defaulting.")
394
  final_system_prompt_str += " Respond directly. (Internal state error)."
395
- final_user_prompt_content_str = f"History:\n{history_str_for_prompt}\nGuidelines:\n{initial_insights_ctx_str}\nQuery: \"{user_input}\"\nResponse:"
396
 
397
 
398
  # --- Final LLM Call for Response Generation ---
@@ -461,12 +461,50 @@ def deferred_learning_and_memory_task(user_input: str, bot_response: str, provid
461
  relevant_existing_rules = retrieve_insights_simple_keywords(reflection_context_query, k_insights=10) # Max 10 rules for context
462
  existing_rules_ctx_str = "\n".join([f"- \"{rule}\"" for rule in relevant_existing_rules]) if relevant_existing_rules else "No specific existing rules found as highly relevant for direct comparison."
463
 
464
- # Use the long system prompt for insight generation from ai-learn
465
- insight_sys_prompt = """You are an expert AI knowledge base curator... (Your full long prompt here from ai-learn's deferred_learning_and_memory)... Output ONLY the JSON list."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  insight_user_prompt = f"""Interaction Summary:\n{summary_for_reflection}\n
467
  Potentially Relevant Existing Rules (Review these carefully for consolidation or refinement):\n{existing_rules_ctx_str}\n
468
  Guiding principles that were considered during THIS interaction (these might offer clues for new rules or refinements):\n{json.dumps([p['original'] for p in parsed_insights_for_reflection if 'original' in p]) if parsed_insights_for_reflection else "None"}\n
469
- Task: Based on your reflection process... (Your full long task description here from ai-learn's deferred_learning_and_memory)... Output JSON only."""
 
 
 
 
470
 
471
  insight_gen_messages = [{"role": "system", "content": insight_sys_prompt}, {"role": "user", "content": insight_user_prompt}]
472
 
@@ -553,13 +591,13 @@ def handle_gradio_chat_submit(user_message_text: str,
553
  ui_api_key_text: str | None,
554
  custom_system_prompt_text: str):
555
 
 
 
556
  # Initialize UI update variables
557
  cleared_input_text = "" # To clear the user input box
558
  updated_gradio_history = list(gradio_chat_history_list) # Copy current display history
559
  current_status_text = "Initializing..."
560
  # Default values for output components to yield immediately
561
- # These should match the types of the output components in demo.launch
562
- # Use dummy values that match the component types if needed
563
  default_detected_outputs_md = gr.Markdown(value="*Processing...*")
564
  default_formatted_output_text = gr.Textbox(value="*Waiting for AI response...*")
565
  default_download_button = gr.DownloadButton(interactive=False, value=None, visible=False)
@@ -586,7 +624,12 @@ def handle_gradio_chat_submit(user_message_text: str,
586
  if len(internal_processing_history) > (MAX_HISTORY_TURNS * 2 + 1): # +1 for potential system prompt
587
  # Simple truncation from the beginning, preserving last N turns
588
  # More sophisticated: keep system prompt if present, then truncate older user/assistant pairs
589
- internal_processing_history = internal_processing_history[-(MAX_HISTORY_TURNS * 2):]
 
 
 
 
 
590
 
591
 
592
  final_bot_response_text_accumulated = ""
@@ -671,8 +714,13 @@ def handle_gradio_chat_submit(user_message_text: str,
671
  current_chat_session_history.append({"role": "assistant", "content": final_bot_response_text_accumulated})
672
 
673
  # Trim global history if it exceeds max turns
674
- if len(current_chat_session_history) > (MAX_HISTORY_TURNS * 2):
675
- current_chat_session_history = current_chat_session_history[-(MAX_HISTORY_TURNS * 2):]
 
 
 
 
 
676
 
677
  # Start deferred learning task in a background thread
678
  logger.info(f"Starting deferred learning task for user: '{user_message_text[:40]}...'")
@@ -705,17 +753,12 @@ def ui_upload_rules_action(uploaded_file_obj, progress=gr.Progress()):
705
  if not uploaded_file_obj: return "No file provided for rules upload."
706
 
707
  try:
708
- content = uploaded_file_obj.decode('utf-8') # Gradio File component gives bytes
709
- except AttributeError: # If it's already a string (e.g. from temp file path)
710
- try:
711
- with open(uploaded_file_obj.name, 'r', encoding='utf-8') as f: # .name if it's a temp file object
712
- content = f.read()
713
- except Exception as e_read:
714
- logger.error(f"Error reading uploaded rules file: {e_read}")
715
- return f"Error reading file: {e_read}"
716
- except Exception as e_decode:
717
- logger.error(f"Error decoding uploaded rules file: {e_decode}")
718
- return f"Error decoding file content: {e_decode}"
719
 
720
  if not content.strip(): return "Uploaded rules file is empty."
721
 
@@ -726,6 +769,7 @@ def ui_upload_rules_action(uploaded_file_obj, progress=gr.Progress()):
726
  potential_rules = [r.strip() for r in content.splitlines() if r.strip()]
727
 
728
  total_to_process = len(potential_rules)
 
729
  progress(0, desc="Starting rules upload...")
730
 
731
  for idx, rule_text in enumerate(potential_rules):
@@ -738,16 +782,66 @@ def ui_upload_rules_action(uploaded_file_obj, progress=gr.Progress()):
738
  # A better save_rule_to_file could return: "added", "duplicate", "invalid_format", "error"
739
 
740
  # Re-check for existing before trying to save for more accurate "skipped_count"
741
- existing_rules_snapshot = load_rules_from_file() # Could be slow if called repeatedly
742
- if rule_text in existing_rules_snapshot:
743
- skipped_count +=1
744
- elif save_rule_to_file(rule_text): # save_rule_to_file will log its own errors/skips
745
- added_count += 1
746
- else: # Failed to save for other reasons (format error logged by save_rule_to_file, or file write error)
747
- error_count += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
748
  progress((idx + 1) / total_to_process, desc=f"Processed {idx+1}/{total_to_process} rules...")
749
 
750
- msg = f"Rules Upload: Processed {total_to_process}. Added: {added_count}, Skipped (duplicates): {skipped_count}, Errors/Not Added: {error_count}."
 
 
 
 
 
 
 
751
  logger.info(msg)
752
  return msg
753
 
@@ -761,17 +855,11 @@ def ui_upload_memories_action(uploaded_file_obj, progress=gr.Progress()):
761
  if not uploaded_file_obj: return "No file provided for memories upload."
762
 
763
  try:
764
- content = uploaded_file_obj.decode('utf-8')
765
- except AttributeError:
766
- try:
767
- with open(uploaded_file_obj.name, 'r', encoding='utf-8') as f:
768
- content = f.read()
769
- except Exception as e_read:
770
- logger.error(f"Error reading uploaded memories file: {e_read}")
771
- return f"Error reading file: {e_read}"
772
- except Exception as e_decode:
773
- logger.error(f"Error decoding uploaded memories file: {e_decode}")
774
- return f"Error decoding file content: {e_decode}"
775
 
776
  if not content.strip(): return "Uploaded memories file is empty."
777
 
@@ -800,17 +888,33 @@ def ui_upload_memories_action(uploaded_file_obj, progress=gr.Progress()):
800
  return "No valid memory objects found in the uploaded file."
801
 
802
  total_to_process = len(memory_objects_to_process)
 
803
  progress(0, desc="Starting memories upload...")
804
 
 
 
 
 
 
 
 
 
 
 
805
  for idx, mem_data in enumerate(memory_objects_to_process):
806
- if not isinstance(mem_data, dict) or not all(k in mem_data for k in ["user_input", "bot_response", "metrics"]): # Timestamp optional for upload
807
  format_error_count += 1
808
  continue
809
 
810
- # For file-based, duplicate check on save might be too slow.
811
- # memory_logic.save_memory_to_file just appends.
 
 
 
 
812
  if save_memory_to_file(mem_data["user_input"], mem_data["bot_response"], mem_data["metrics"]):
813
  added_count += 1
 
814
  else:
815
  save_error_count += 1 # Error during file write
816
  progress((idx + 1) / total_to_process, desc=f"Processed {idx+1}/{total_to_process} memories...")
@@ -974,7 +1078,8 @@ if __name__ == "__main__":
974
  # No explicit data loading into globals needed here if handlers load on demand.
975
 
976
  app_port = int(os.getenv("GRADIO_PORT", 7860))
977
- app_server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")
 
978
  app_debug_mode = os.getenv("GRADIO_DEBUG", "False").lower() == "true"
979
  app_share_mode = os.getenv("GRADIO_SHARE", "False").lower() == "true"
980
 
 
358
  logger.warning(f"PUI_GRADIO [{request_id}]: Missing input for {action_type}. Falling back.")
359
  action_type = "quick_respond" # Fallback to quick_respond logic above
360
  final_system_prompt_str += " Respond directly. (Note: A web action was attempted but failed due to missing input)."
361
+ final_user_prompt_content_str = f"Current Conversation History (User/AI turns):\n{history_str_for_prompt}\n\nGuiding Principles (Learned Rules):\n{initial_insights_ctx_str}\n\nUser's Current Query: \"{user_input}\"\n\nYour concise and helpful response:"
362
  else:
363
  yield "status", f"<i>[Web: '{query_or_url[:60]}'...]</i>"
364
  web_results_data = []
 
387
 
388
  yield "status", "<i>[Synthesizing web report...]</i>"
389
  final_system_prompt_str += " Generate a report/answer from web content, history, & guidelines. Cite URLs as [Source X]."
390
+ final_user_prompt_content_str = f"Current Conversation History:\n{history_str_for_prompt}\n\nGuiding Principles:\n{initial_insights_ctx_str}\n\nWeb Content Found:\n{scraped_content_for_prompt}\n\nUser's Query: \"{user_input}\"\n\nYour report/response (cite sources like [Source 1], [Source 2]):"
391
 
392
  else: # Fallback if action_type is somehow unknown
393
  logger.warning(f"PUI_GRADIO [{request_id}]: Unknown action_type '{action_type}'. Defaulting.")
394
  final_system_prompt_str += " Respond directly. (Internal state error)."
395
+ final_user_prompt_content_str = f"Current Conversation History (User/AI turns):\n{history_str_for_prompt}\n\nGuiding Principles (Learned Rules):\n{initial_insights_ctx_str}\n\nUser's Current Query: \"{user_input}\"\n\nYour concise and helpful response:"
396
 
397
 
398
  # --- Final LLM Call for Response Generation ---
 
461
  relevant_existing_rules = retrieve_insights_simple_keywords(reflection_context_query, k_insights=10) # Max 10 rules for context
462
  existing_rules_ctx_str = "\n".join([f"- \"{rule}\"" for rule in relevant_existing_rules]) if relevant_existing_rules else "No specific existing rules found as highly relevant for direct comparison."
463
 
464
+ # Insight generation prompt (ensure this is the full, detailed prompt from ai-learn)
465
+ insight_sys_prompt = """You are an expert AI knowledge base curator. Your primary function is to meticulously analyze an interaction and update the AI's guiding principles (insights/rules) to improve its future performance and self-understanding.
466
+ You MUST output a JSON list of operation objects. This list can and SHOULD contain MULTIPLE distinct operations if various learnings occurred.
467
+ Each operation object in the JSON list must have:
468
+ 1. "action": A string, either "add" (for entirely new rules) or "update" (to replace an existing rule with a better one).
469
+ 2. "insight": A string, the full, refined insight text including its [TYPE|SCORE] prefix (e.g., "[CORE_RULE|1.0] My name is Lumina, an AI assistant.").
470
+ 3. "old_insight_to_replace" (ONLY for "update" action): A string, the *exact, full text* of an existing insight that the new "insight" should replace.
471
+ **Your Reflection Process (Consider each step and generate operations accordingly):**
472
+ **STEP 1: Core Identity & Purpose Review (Result: Primarily 'update' operations)**
473
+ - Examine all `CORE_RULE`s related to my identity (name, fundamental purpose, core unchanging capabilities, origin) from the "Potentially Relevant Existing Rules".
474
+ - **CONSOLIDATE & MERGE:** If multiple `CORE_RULE`s state similar aspects (e.g., multiple name declarations like 'Lumina' and 'LearnerAI', or slightly different purpose statements), you MUST merge them into ONE definitive, comprehensive `CORE_RULE`.
475
+ - The new "insight" will be this single, merged rule. Propose separate "update" operations to replace *each* redundant or less accurate core identity rule with this new canonical one.
476
+ - Prioritize user-assigned names or the most specific, recently confirmed information. If the interaction summary clarifies a name or core function, ensure this is reflected.
477
+ **STEP 2: New Distinct Learnings (Result: Primarily 'add' operations)**
478
+ - Did I learn any completely new, distinct facts (e.g., "The user's project is codenamed 'Bluefire'")?
479
+ - Did I demonstrate or get told about a new skill/capability not previously documented (e.g., "I can now generate mermaid diagrams based on descriptions")?
480
+ - Did the user express a strong, general preference that should guide future interactions (e.g., "User prefers responses to start with a direct answer, then explanation")?
481
+ - For these, propose 'add' operations. Assign `CORE_RULE` for truly fundamental new facts/capabilities, otherwise `RESPONSE_PRINCIPLE` or `BEHAVIORAL_ADJUSTMENT`. Ensure these are genuinely NEW and not just rephrasing of existing non-core rules.
482
+ **STEP 3: Refinements to Existing Behaviors/Principles (Result: 'update' operations for non-core rules)**
483
+ - Did I learn to modify or improve an existing behavior, response style, or operational guideline (that is NOT part of core identity)?
484
+ - For example, if an existing `RESPONSE_PRINCIPLE` was "Be formal," and the interaction showed the user prefers informality, update that principle.
485
+ - Propose 'update' operations for the relevant `RESPONSE_PRINCIPLE` or `BEHAVIORAL_ADJUSTMENT`. Only update if the change is significant.
486
+ **General Guidelines:**
487
+ - If no new insights, updates, or consolidations are warranted from the interaction, output an empty JSON list: `[]`.
488
+ - Ensure the "insight" field (for both add/update) always contains the properly formatted insight string: `[TYPE|SCORE] Text`. TYPE can be `CORE_RULE`, `RESPONSE_PRINCIPLE`, `BEHAVIORAL_ADJUSTMENT`. Scores should reflect confidence/importance.
489
+ - Be precise with "old_insight_to_replace" – it must *exactly* match an existing rule string from the "Potentially Relevant Existing Rules" context.
490
+ - Aim for a comprehensive set of operations that reflects ALL key learnings from the interaction.
491
+ - Output ONLY the JSON list. No other text, explanations, or markdown.
492
+ **Example of a comprehensive JSON output with MULTIPLE operations:**
493
+ [
494
+ {"action": "update", "old_insight_to_replace": "[CORE_RULE|1.0] My designated name is 'LearnerAI'.", "insight": "[CORE_RULE|1.0] I am Lumina, an AI assistant designed to chat, provide information, and remember context like the secret word 'rocksyrup'."},
495
+ {"action": "update", "old_insight_to_replace": "[CORE_RULE|1.0] I'm Lumina, the AI designed to chat with you.", "insight": "[CORE_RULE|1.0] I am Lumina, an AI assistant designed to chat, provide information, and remember context like the secret word 'rocksyrup'."},
496
+ {"action": "add", "insight": "[CORE_RULE|0.9] I am capable of searching the internet for current weather information if asked."},
497
+ {"action": "add", "insight": "[RESPONSE_PRINCIPLE|0.8] When user provides positive feedback, acknowledge it warmly."},
498
+ {"action": "update", "old_insight_to_replace": "[RESPONSE_PRINCIPLE|0.7] Avoid mentioning old conversations.", "insight": "[RESPONSE_PRINCIPLE|0.85] Avoid mentioning old conversations unless the user explicitly refers to them or it's highly relevant to the current query."}
499
+ ]"""
500
  insight_user_prompt = f"""Interaction Summary:\n{summary_for_reflection}\n
501
  Potentially Relevant Existing Rules (Review these carefully for consolidation or refinement):\n{existing_rules_ctx_str}\n
502
  Guiding principles that were considered during THIS interaction (these might offer clues for new rules or refinements):\n{json.dumps([p['original'] for p in parsed_insights_for_reflection if 'original' in p]) if parsed_insights_for_reflection else "None"}\n
503
+ Task: Based on your three-step reflection process (Core Identity, New Learnings, Refinements):
504
+ 1. **Consolidate CORE_RULEs:** Merge similar identity/purpose rules from "Potentially Relevant Existing Rules" into single, definitive statements using "update" operations. Replace multiple old versions with the new canonical one.
505
+ 2. **Add New Learnings:** Identify and "add" any distinct new facts, skills, or important user preferences learned from the "Interaction Summary".
506
+ 3. **Update Existing Principles:** "Update" any non-core principles from "Potentially Relevant Existing Rules" if the "Interaction Summary" provided a clear refinement.
507
+ Combine all findings into a single JSON list of operations. If there are multiple distinct changes based on the interaction and existing rules, ensure your list reflects all of them. Output JSON only."""
508
 
509
  insight_gen_messages = [{"role": "system", "content": insight_sys_prompt}, {"role": "user", "content": insight_user_prompt}]
510
 
 
591
  ui_api_key_text: str | None,
592
  custom_system_prompt_text: str):
593
 
594
+ global current_chat_session_history # <--- ***** ADD THIS LINE *****
595
+
596
  # Initialize UI update variables
597
  cleared_input_text = "" # To clear the user input box
598
  updated_gradio_history = list(gradio_chat_history_list) # Copy current display history
599
  current_status_text = "Initializing..."
600
  # Default values for output components to yield immediately
 
 
601
  default_detected_outputs_md = gr.Markdown(value="*Processing...*")
602
  default_formatted_output_text = gr.Textbox(value="*Waiting for AI response...*")
603
  default_download_button = gr.DownloadButton(interactive=False, value=None, visible=False)
 
624
  if len(internal_processing_history) > (MAX_HISTORY_TURNS * 2 + 1): # +1 for potential system prompt
625
  # Simple truncation from the beginning, preserving last N turns
626
  # More sophisticated: keep system prompt if present, then truncate older user/assistant pairs
627
+ # This simple truncation might lose a system prompt if it was the very first item
628
+ # A better approach if system prompts are always first:
629
+ if internal_processing_history[0]["role"] == "system" and len(internal_processing_history) > (MAX_HISTORY_TURNS * 2 +1) :
630
+ internal_processing_history = [internal_processing_history[0]] + internal_processing_history[-(MAX_HISTORY_TURNS * 2):]
631
+ else:
632
+ internal_processing_history = internal_processing_history[-(MAX_HISTORY_TURNS * 2):]
633
 
634
 
635
  final_bot_response_text_accumulated = ""
 
714
  current_chat_session_history.append({"role": "assistant", "content": final_bot_response_text_accumulated})
715
 
716
  # Trim global history if it exceeds max turns
717
+ if len(current_chat_session_history) > (MAX_HISTORY_TURNS * 2): # Check total length
718
+ # If a system prompt is the first message, preserve it during trimming
719
+ if current_chat_session_history[0]["role"] == "system" and len(current_chat_session_history) > (MAX_HISTORY_TURNS * 2 + 1):
720
+ current_chat_session_history = [current_chat_session_history[0]] + current_chat_session_history[-(MAX_HISTORY_TURNS * 2):]
721
+ else: # No system prompt or length is fine with system prompt
722
+ current_chat_session_history = current_chat_session_history[-(MAX_HISTORY_TURNS * 2):]
723
+
724
 
725
  # Start deferred learning task in a background thread
726
  logger.info(f"Starting deferred learning task for user: '{user_message_text[:40]}...'")
 
753
  if not uploaded_file_obj: return "No file provided for rules upload."
754
 
755
  try:
756
+ # Gradio's File component gives a temp file object, access its name
757
+ with open(uploaded_file_obj.name, 'r', encoding='utf-8') as f:
758
+ content = f.read()
759
+ except Exception as e_read:
760
+ logger.error(f"Error reading uploaded rules file: {e_read}")
761
+ return f"Error reading file: {e_read}"
 
 
 
 
 
762
 
763
  if not content.strip(): return "Uploaded rules file is empty."
764
 
 
769
  potential_rules = [r.strip() for r in content.splitlines() if r.strip()]
770
 
771
  total_to_process = len(potential_rules)
772
+ if total_to_process == 0: return "No rules found in file to process."
773
  progress(0, desc="Starting rules upload...")
774
 
775
  for idx, rule_text in enumerate(potential_rules):
 
782
  # A better save_rule_to_file could return: "added", "duplicate", "invalid_format", "error"
783
 
784
  # Re-check for existing before trying to save for more accurate "skipped_count"
785
+ # This is inefficient for very large rule sets, but ok for moderate numbers.
786
+ # Consider optimizing if this becomes a bottleneck.
787
+ # existing_rules_snapshot = load_rules_from_file()
788
+ # if rule_text in existing_rules_snapshot:
789
+ # skipped_count +=1
790
+ # el
791
+ if save_rule_to_file(rule_text): # save_rule_to_file logs its own errors/skips for duplicates/format
792
+ # save_rule_to_file returns True if saved, False if duplicate, invalid, or error
793
+ # This logic needs refinement if save_rule_to_file doesn't distinguish duplicate vs error
794
+ # Assuming save_rule_to_file now only returns True on actual addition.
795
+ # The memory_logic.py version of save_rule_to_file returns False for duplicates already.
796
+ added_count +=1
797
+ else:
798
+ # Check if it was a duplicate to categorize skipped_count, otherwise it's an error_count
799
+ # This requires knowing if the rule format was valid first. save_rule_to_file does this.
800
+ # If save_rule_to_file returned False, it means it was either a duplicate, invalid format, or save error.
801
+ # To simplify UI feedback, we'll just count as 'not added'.
802
+ # More detailed feedback would require save_rule_to_file to return distinct status codes.
803
+ # For now, if it wasn't added, and we can't easily tell if it was a duplicate vs format error here,
804
+ # let's assume it's an error for this counter, as save_rule_to_file already logs duplicates.
805
+ is_valid_format = bool(re.match(r"\[(CORE_RULE|RESPONSE_PRINCIPLE|BEHAVIORAL_ADJUSTMENT|GENERAL_LEARNING)\|([\d\.]+?)\](.*)", rule_text, re.I|re.DOTALL))
806
+ if not is_valid_format: # It was a format error
807
+ error_count += 1
808
+ else: # It was a duplicate (already logged by save_rule_to_file) or a file write error
809
+ # if rule_text in load_rules_from_file(): # this check is redundant if save_rule_to_file does it
810
+ # skipped_count +=1
811
+ # else: # True error
812
+ # error_count += 1
813
+ # To avoid re-loading rules file constantly, rely on save_rule_to_file's logging.
814
+ # If it returns False and it wasn't added, it's complex to distinguish here without more info.
815
+ # Let's simplify: if not added, increment a general "not_added_or_error" count.
816
+ # The current save_rule_to_file will log "already exists". We need to avoid double counting.
817
+ # Solution: save_rule_to_file should be the sole decider.
818
+ # Let's adjust how we count based on a hypothetical enhanced save_rule_to_file:
819
+ # result = save_rule_to_file_detailed(rule_text)
820
+ # if result == "added": added_count += 1
821
+ # elif result == "duplicate": skipped_count += 1
822
+ # elif result == "invalid_format": error_count += 1
823
+ # else: error_count +=1 # file write error
824
+ # For now, with current save_rule_to_file:
825
+ # The logic in save_rule_to_file already prevents adding duplicates and invalid formats.
826
+ # So, if save_rule_to_file returns False, it means it was a duplicate, invalid, or actual save error.
827
+ # We need better differentiation for accurate counts.
828
+ # This simple count will be approximate for "skipped" vs "error".
829
+ # The provided save_rule_to_file handles duplicates and format errors internally and returns False.
830
+ # We can't easily tell which from just "False" here.
831
+ # So we'll assume False means it wasn't added for *some* reason (duplicate, format, error).
832
+ pass # The `save_rule_to_file` will log the reason if it's a duplicate or format error.
833
+ # If it's a true file write error, that's also logged.
834
+ # For UI, we can only show how many were *successfully* added.
835
  progress((idx + 1) / total_to_process, desc=f"Processed {idx+1}/{total_to_process} rules...")
836
 
837
+ # After loop, get final counts for summary.
838
+ # This means we must load rules again to find out how many are new vs existing for skipped_count.
839
+ # This is inefficient. A better `save_rule_to_file` that returns status is needed for good UI feedback.
840
+ # For now, the UI feedback will be simpler:
841
+ final_rules_list = load_rules_from_file()
842
+ actual_added_this_run = added_count # This count is based on `save_rule_to_file` returning True only for new, valid rules.
843
+
844
+ msg = f"Rules Upload: Attempted {total_to_process}. Successfully added: {actual_added_this_run}. Others skipped (duplicates/invalid format/errors - check logs)."
845
  logger.info(msg)
846
  return msg
847
 
 
855
  if not uploaded_file_obj: return "No file provided for memories upload."
856
 
857
  try:
858
+ with open(uploaded_file_obj.name, 'r', encoding='utf-8') as f:
859
+ content = f.read()
860
+ except Exception as e_read:
861
+ logger.error(f"Error reading uploaded memories file: {e_read}")
862
+ return f"Error reading file: {e_read}"
 
 
 
 
 
 
863
 
864
  if not content.strip(): return "Uploaded memories file is empty."
865
 
 
888
  return "No valid memory objects found in the uploaded file."
889
 
890
  total_to_process = len(memory_objects_to_process)
891
+ if total_to_process == 0: return "No memory objects to process after parsing."
892
  progress(0, desc="Starting memories upload...")
893
 
894
+ # Duplicate check for memories is harder without unique IDs.
895
+ # `save_memory_to_file` currently just appends.
896
+ # For simplicity, this upload will append all validly formatted memories.
897
+ # A more robust system would check for duplicates based on content signature or timestamp.
898
+ existing_memories_timestamps_content = set() # For basic duplicate check in this upload session
899
+ # Pre-load existing to avoid adding true duplicates if possible
900
+ # for mem in load_memories_from_file():
901
+ # existing_memories_timestamps_content.add((mem.get("timestamp"), mem.get("user_input"), mem.get("bot_response")))
902
+
903
+
904
  for idx, mem_data in enumerate(memory_objects_to_process):
905
+ if not isinstance(mem_data, dict) or not all(k in mem_data for k in ["user_input", "bot_response", "metrics"]): # Timestamp is good to have but save_memory_to_file adds it if missing
906
  format_error_count += 1
907
  continue
908
 
909
+ # Simple duplicate check for this session (not against all stored if large)
910
+ # current_sig = (mem_data.get("timestamp"), mem_data.get("user_input"), mem_data.get("bot_response"))
911
+ # if current_sig in existing_memories_timestamps_content:
912
+ # # This is a duplicate from the file itself or already in store
913
+ # continue # Skip adding
914
+
915
  if save_memory_to_file(mem_data["user_input"], mem_data["bot_response"], mem_data["metrics"]):
916
  added_count += 1
917
+ # existing_memories_timestamps_content.add(current_sig) # Add to session set
918
  else:
919
  save_error_count += 1 # Error during file write
920
  progress((idx + 1) / total_to_process, desc=f"Processed {idx+1}/{total_to_process} memories...")
 
1078
  # No explicit data loading into globals needed here if handlers load on demand.
1079
 
1080
  app_port = int(os.getenv("GRADIO_PORT", 7860))
1081
+ # Default to 127.0.0.1 for local access unless 0.0.0.0 is explicitly set for broader network access
1082
+ app_server_name = os.getenv("GRADIO_SERVER_NAME", "127.0.0.1")
1083
  app_debug_mode = os.getenv("GRADIO_DEBUG", "False").lower() == "true"
1084
  app_share_mode = os.getenv("GRADIO_SHARE", "False").lower() == "true"
1085