tinyagent-py 0.0.9__py3-none-any.whl → 0.0.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62,6 +62,7 @@ class TinyCodeAgent:
62
62
  self.user_variables = user_variables or {}
63
63
  self.pip_packages = pip_packages or []
64
64
  self.local_execution = local_execution
65
+ self.provider = provider # Store provider type for reuse
65
66
 
66
67
  # Create the code execution provider
67
68
  self.code_provider = self._create_provider(provider, self.provider_config)
@@ -96,8 +97,13 @@ class TinyCodeAgent:
96
97
  config_pip_packages = config.get("pip_packages", [])
97
98
  final_pip_packages = list(set(self.pip_packages + config_pip_packages))
98
99
 
100
+ # Merge authorized_imports from both sources (direct parameter and provider_config)
101
+ config_authorized_imports = config.get("authorized_imports", [])
102
+ final_authorized_imports = list(set(self.authorized_imports + config_authorized_imports))
103
+
99
104
  final_config = config.copy()
100
105
  final_config["pip_packages"] = final_pip_packages
106
+ final_config["authorized_imports"] = final_authorized_imports
101
107
 
102
108
  return ModalProvider(
103
109
  log_manager=self.log_manager,
@@ -258,6 +264,14 @@ class TinyCodeAgent:
258
264
  result = await self.code_provider.execute_python(code_lines, timeout)
259
265
  return str(result)
260
266
  except Exception as e:
267
+ print("!"*100)
268
+ COLOR = {
269
+ "RED": "\033[91m",
270
+ "ENDC": "\033[0m",
271
+ }
272
+ print(f"{COLOR['RED']}{str(e)}{COLOR['ENDC']}")
273
+ print(f"{COLOR['RED']}{traceback.format_exc()}{COLOR['ENDC']}")
274
+ print("!"*100)
261
275
  return f"Error executing code: {str(e)}"
262
276
 
263
277
  self.agent.add_tool(run_python)
@@ -441,6 +455,69 @@ class TinyCodeAgent:
441
455
  """
442
456
  return self.pip_packages.copy()
443
457
 
458
+ def add_authorized_imports(self, imports: List[str]):
459
+ """
460
+ Add additional authorized imports to the execution environment.
461
+
462
+ Args:
463
+ imports: List of import names to authorize
464
+ """
465
+ self.authorized_imports.extend(imports)
466
+ self.authorized_imports = list(set(self.authorized_imports)) # Remove duplicates
467
+
468
+ # Update the provider with the new authorized imports
469
+ # This requires recreating the provider
470
+ print("⚠️ Warning: Adding authorized imports after initialization requires recreating the Modal environment.")
471
+ print(" For better performance, set authorized_imports during TinyCodeAgent initialization.")
472
+
473
+ # Recreate the provider with new authorized imports
474
+ self.code_provider = self._create_provider(self.provider, self.provider_config)
475
+
476
+ # Re-set user variables if they exist
477
+ if self.user_variables:
478
+ self.code_provider.set_user_variables(self.user_variables)
479
+
480
+ # Rebuild system prompt to include new authorized imports
481
+ self.system_prompt = self._build_system_prompt()
482
+ # Update the agent's system prompt
483
+ self.agent.system_prompt = self.system_prompt
484
+
485
+ def get_authorized_imports(self) -> List[str]:
486
+ """
487
+ Get a copy of current authorized imports.
488
+
489
+ Returns:
490
+ List of authorized imports
491
+ """
492
+ return self.authorized_imports.copy()
493
+
494
+ def remove_authorized_import(self, import_name: str):
495
+ """
496
+ Remove an authorized import.
497
+
498
+ Args:
499
+ import_name: Import name to remove
500
+ """
501
+ if import_name in self.authorized_imports:
502
+ self.authorized_imports.remove(import_name)
503
+
504
+ # Update the provider with the new authorized imports
505
+ # This requires recreating the provider
506
+ print("⚠️ Warning: Removing authorized imports after initialization requires recreating the Modal environment.")
507
+ print(" For better performance, set authorized_imports during TinyCodeAgent initialization.")
508
+
509
+ # Recreate the provider with updated authorized imports
510
+ self.code_provider = self._create_provider(self.provider, self.provider_config)
511
+
512
+ # Re-set user variables if they exist
513
+ if self.user_variables:
514
+ self.code_provider.set_user_variables(self.user_variables)
515
+
516
+ # Rebuild system prompt to reflect updated authorized imports
517
+ self.system_prompt = self._build_system_prompt()
518
+ # Update the agent's system prompt
519
+ self.agent.system_prompt = self.system_prompt
520
+
444
521
  async def close(self):
445
522
  """Clean up resources."""
446
523
  await self.code_provider.cleanup()
@@ -498,6 +575,7 @@ async def run_example():
498
575
  user_variables={
499
576
  "sample_data": [1, 2, 3, 4, 5, 10, 15, 20]
500
577
  },
578
+ authorized_imports=["tinyagent", "gradio", "requests", "numpy", "pandas"], # Explicitly specify authorized imports
501
579
  local_execution=False # Remote execution via Modal (default)
502
580
  )
503
581
 
@@ -524,6 +602,7 @@ async def run_example():
524
602
  user_variables={
525
603
  "sample_data": [1, 2, 3, 4, 5, 10, 15, 20]
526
604
  },
605
+ authorized_imports=["tinyagent", "gradio", "requests"], # More restricted imports for local execution
527
606
  local_execution=True # Local execution
528
607
  )
529
608
 
@@ -550,6 +629,18 @@ async def run_example():
550
629
  agent_remote.add_code_tool(validator)
551
630
  agent_local.add_code_tool(validator)
552
631
 
632
+ # Demonstrate adding authorized imports dynamically
633
+ print("\n" + "="*80)
634
+ print("🔧 Testing with dynamically added authorized imports")
635
+ agent_remote.add_authorized_imports(["matplotlib", "seaborn"])
636
+
637
+ # Test with visualization libraries
638
+ viz_prompt = "Create a simple plot of the sample_data and save it as a base64 encoded image string."
639
+
640
+ response_viz = await agent_remote.run(viz_prompt)
641
+ print("Remote Agent Visualization Response:")
642
+ print(response_viz)
643
+
553
644
  print("\n" + "="*80)
554
645
  print("🔧 Testing with dynamically added tools")
555
646
 
@@ -1,6 +1,7 @@
1
1
  import sys
2
2
  import cloudpickle
3
- from typing import Dict, Any
3
+ from typing import Dict, Any, List
4
+ from .safety import validate_code_safety, function_safety_context
4
5
 
5
6
 
6
7
  def clean_response(resp: Dict[str, Any]) -> Dict[str, Any]:
@@ -40,7 +41,14 @@ def make_session_blob(ns: dict) -> bytes:
40
41
  return cloudpickle.dumps(clean)
41
42
 
42
43
 
43
- def _run_python(code: str, globals_dict: Dict[str, Any] = None, locals_dict: Dict[str, Any] = None):
44
+ def _run_python(
45
+ code: str,
46
+ globals_dict: Dict[str, Any] | None = None,
47
+ locals_dict: Dict[str, Any] | None = None,
48
+ authorized_imports: List[str] | None = None,
49
+ authorized_functions: List[str] | None = None,
50
+ trusted_code: bool = False,
51
+ ):
44
52
  """
45
53
  Execute Python code in a controlled environment with proper error handling.
46
54
 
@@ -48,6 +56,9 @@ def _run_python(code: str, globals_dict: Dict[str, Any] = None, locals_dict: Dic
48
56
  code: Python code to execute
49
57
  globals_dict: Global variables dictionary
50
58
  locals_dict: Local variables dictionary
59
+ authorized_imports: List of authorized imports that user code may access. Wildcards (e.g. "numpy.*") are supported. A value of None disables the allow-list and only blocks dangerous modules.
60
+ authorized_functions: List of authorized dangerous functions that user code may access. A value of None disables the allow-list and blocks all dangerous functions.
61
+ trusted_code: If True, skip security checks. Should only be used for framework code, tools, or default executed code.
51
62
 
52
63
  Returns:
53
64
  Dictionary containing execution results
@@ -56,16 +67,27 @@ def _run_python(code: str, globals_dict: Dict[str, Any] = None, locals_dict: Dic
56
67
  import traceback
57
68
  import io
58
69
  import ast
59
-
70
+ import builtins # Needed for import hook
71
+ import sys
72
+
73
+ # ------------------------------------------------------------------
74
+ # 1. Static safety analysis – refuse code containing dangerous imports or functions
75
+ # ------------------------------------------------------------------
76
+ validate_code_safety(code, authorized_imports=authorized_imports,
77
+ authorized_functions=authorized_functions, trusted_code=trusted_code)
78
+
60
79
  # Make copies to avoid mutating the original parameters
61
80
  globals_dict = globals_dict or {}
62
81
  locals_dict = locals_dict or {}
63
82
  updated_globals = globals_dict.copy()
64
83
  updated_locals = locals_dict.copy()
65
84
 
66
- # Pre-import essential modules into the global namespace
67
- # This ensures they're available for imports inside functions
68
- essential_modules = ['requests', 'json', 'os', 'sys', 'time', 'datetime', 're', 'random', 'math']
85
+ # Only pre-import a **minimal** set of safe modules so that common helper
86
+ # functions work out of the box without giving user code access to the
87
+ # full standard library. Anything outside this list must be imported
88
+ # explicitly by the user – and will be blocked by the safety layer above
89
+ # if considered dangerous.
90
+ essential_modules = ['requests', 'json', 'time', 'datetime', 're', 'random', 'math','cloudpickle']
69
91
 
70
92
  for module_name in essential_modules:
71
93
  try:
@@ -75,12 +97,30 @@ def _run_python(code: str, globals_dict: Dict[str, Any] = None, locals_dict: Dic
75
97
  except ImportError:
76
98
  print(f"⚠️ Warning: {module_name} module not available")
77
99
 
100
+ # Variable to store print output
101
+ output_buffer = []
102
+
103
+ # Create a custom print function that captures output
104
+ def custom_print(*args, **kwargs):
105
+ # Get the sep and end kwargs, defaulting to ' ' and '\n'
106
+ sep = kwargs.get('sep', ' ')
107
+ end = kwargs.get('end', '\n')
108
+
109
+ # Convert all arguments to strings and join them
110
+ output = sep.join(str(arg) for arg in args) + end
111
+
112
+ # Store the output
113
+ output_buffer.append(output)
114
+
115
+ # Add the custom print function to the globals
116
+ #updated_globals['print'] = custom_print
117
+
118
+ # Parse the code
78
119
  tree = ast.parse(code, mode="exec")
79
120
  compiled = compile(tree, filename="<ast>", mode="exec")
80
121
  stdout_buf = io.StringIO()
81
- stderr_buf = io.StringIO()
82
-
83
- # Execute with stdout+stderr capture and exception handling
122
+ stderr_buf = io.StringIO()
123
+ # Execute with exception handling
84
124
  error_traceback = None
85
125
  output = None
86
126
 
@@ -92,8 +132,15 @@ def _run_python(code: str, globals_dict: Dict[str, Any] = None, locals_dict: Dic
92
132
  merged_globals = updated_globals.copy()
93
133
  merged_globals.update(updated_locals)
94
134
 
135
+ # Add 'exec' to authorized_functions for internal use
136
+ internal_authorized_functions = ['exec','eval']
137
+ if authorized_functions is not None and not isinstance(authorized_functions, bool):
138
+ internal_authorized_functions.extend(authorized_functions)
139
+
95
140
  # Execute with only globals - this fixes generator expression scoping issues
96
- output = exec(code, merged_globals)
141
+ # Use the function_safety_context to block dangerous functions during execution
142
+ with function_safety_context(authorized_functions=internal_authorized_functions, trusted_code=trusted_code):
143
+ output = exec(compiled, merged_globals)
97
144
 
98
145
  # Update both dictionaries with any new variables created during execution
99
146
  for key, value in merged_globals.items():
@@ -106,6 +153,8 @@ def _run_python(code: str, globals_dict: Dict[str, Any] = None, locals_dict: Dic
106
153
  # Capture the full traceback as a string
107
154
  error_traceback = traceback.format_exc()
108
155
 
156
+ # Join all captured output
157
+ #printed_output = ''.join(output_buffer)
109
158
  printed_output = stdout_buf.getvalue()
110
159
  stderr_output = stderr_buf.getvalue()
111
160
  error_traceback_output = error_traceback
@@ -5,6 +5,7 @@ import os
5
5
  import re
6
6
  import shutil
7
7
  import time
8
+ import io
8
9
  from pathlib import Path
9
10
  from typing import Any, Dict, List, Optional, Set, Union
10
11
 
@@ -36,6 +37,7 @@ class GradioCallback:
36
37
  show_thinking: bool = True,
37
38
  show_tool_calls: bool = True,
38
39
  logger: Optional[logging.Logger] = None,
40
+ log_manager: Optional[Any] = None,
39
41
  ):
40
42
  """
41
43
  Initialize the Gradio callback.
@@ -46,6 +48,7 @@ class GradioCallback:
46
48
  show_thinking: Whether to show the thinking process
47
49
  show_tool_calls: Whether to show tool calls
48
50
  logger: Optional logger to use
51
+ log_manager: Optional LoggingManager instance to capture logs from
49
52
  """
50
53
  self.logger = logger or logging.getLogger(__name__)
51
54
  self.show_thinking = show_thinking
@@ -81,6 +84,37 @@ class GradioCallback:
81
84
  # References to Gradio UI components (will be set in create_app)
82
85
  self._chatbot_component = None
83
86
  self._token_usage_component = None
87
+
88
+ # Log stream for displaying logs in the UI
89
+ self.log_stream = io.StringIO()
90
+ self._log_component = None
91
+
92
+ # Setup logging
93
+ self.log_manager = log_manager
94
+ if log_manager:
95
+ # Create a handler that writes to our StringIO stream
96
+ self.log_handler = logging.StreamHandler(self.log_stream)
97
+ self.log_handler.setFormatter(
98
+ logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
99
+ )
100
+ self.log_handler.setLevel(logging.DEBUG)
101
+
102
+ # Add the handler to the LoggingManager
103
+ log_manager.configure_handler(
104
+ self.log_handler,
105
+ format_string='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
106
+ level=logging.DEBUG
107
+ )
108
+ self.logger.debug("Added log handler to LoggingManager")
109
+ elif logger:
110
+ # Fall back to single logger if no LoggingManager is provided
111
+ self.log_handler = logging.StreamHandler(self.log_stream)
112
+ self.log_handler.setFormatter(
113
+ logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
114
+ )
115
+ self.log_handler.setLevel(logging.DEBUG)
116
+ logger.addHandler(self.log_handler)
117
+ self.logger.debug("Added log handler to logger")
84
118
 
85
119
  self.logger.debug("GradioCallback initialized")
86
120
 
@@ -525,7 +559,7 @@ class GradioCallback:
525
559
  typing_message_index = len(chatbot_history) - 1
526
560
 
527
561
  # Initial yield to show user message and typing indicator
528
- yield chatbot_history, self._get_token_usage_text()
562
+ yield chatbot_history, self._get_token_usage_text(), self.log_stream.getvalue() if self._log_component else None
529
563
 
530
564
  # Kick off the agent in the background
531
565
  loop = asyncio.get_event_loop()
@@ -632,9 +666,10 @@ class GradioCallback:
632
666
  del in_progress_tool_calls[tid]
633
667
  self.logger.debug(f"Updated tool call to completed: {tname}")
634
668
 
635
- # yield updated history + token usage
669
+ # yield updated history + token usage + logs
636
670
  token_text = self._get_token_usage_text()
637
- yield chatbot_history, token_text
671
+ logs = self.log_stream.getvalue() if self._log_component else None
672
+ yield chatbot_history, token_text, logs
638
673
  self.last_update_yield_time = now
639
674
 
640
675
  await asyncio.sleep(update_interval)
@@ -657,8 +692,9 @@ class GradioCallback:
657
692
  )
658
693
  self.logger.debug(f"Added final result: {final_text[:50]}...")
659
694
 
660
- # final token usage
661
- yield chatbot_history, self._get_token_usage_text()
695
+ # final token usage and logs
696
+ logs = self.log_stream.getvalue() if self._log_component else None
697
+ yield chatbot_history, self._get_token_usage_text(), logs
662
698
 
663
699
  def _format_response(self, response_text):
664
700
  """
@@ -839,6 +875,22 @@ class GradioCallback:
839
875
  # Clear button
840
876
  clear_btn = gr.Button("Clear Conversation")
841
877
 
878
+ # Log accordion - similar to the example provided
879
+ with gr.Accordion("Agent Logs", open=False) as log_accordion:
880
+ self._log_component = gr.Code(
881
+ label="Live Logs",
882
+ lines=15,
883
+ interactive=False,
884
+ value=self.log_stream.getvalue()
885
+ )
886
+ refresh_logs_btn = gr.Button("🔄 Refresh Logs")
887
+ refresh_logs_btn.click(
888
+ fn=lambda: self.log_stream.getvalue(),
889
+ inputs=None,
890
+ outputs=[self._log_component],
891
+ queue=False
892
+ )
893
+
842
894
  # Store processed input temporarily between steps
843
895
  processed_input_state = gr.State("")
844
896
 
@@ -859,7 +911,7 @@ class GradioCallback:
859
911
  # 3. Run the main interaction loop (this yields updates)
860
912
  fn=self.interact_with_agent,
861
913
  inputs=[processed_input_state, self._chatbot_component],
862
- outputs=[self._chatbot_component, self._token_usage_component], # Update chat and tokens
914
+ outputs=[self._chatbot_component, self._token_usage_component, self._log_component], # Update chat, tokens, and logs
863
915
  queue=True # Explicitly enable queue for this async generator
864
916
  ).then(
865
917
  # 4. Re-enable the button after interaction finishes
@@ -885,7 +937,7 @@ class GradioCallback:
885
937
  # 3. Run the main interaction loop (this yields updates)
886
938
  fn=self.interact_with_agent,
887
939
  inputs=[processed_input_state, self._chatbot_component],
888
- outputs=[self._chatbot_component, self._token_usage_component], # Update chat and tokens
940
+ outputs=[self._chatbot_component, self._token_usage_component, self._log_component], # Update chat, tokens, and logs
889
941
  queue=True # Explicitly enable queue for this async generator
890
942
  ).then(
891
943
  # 4. Re-enable the button after interaction finishes
@@ -899,8 +951,8 @@ class GradioCallback:
899
951
  clear_btn.click(
900
952
  fn=self.clear_conversation,
901
953
  inputs=None, # No inputs needed
902
- # Outputs: Clear chatbot and reset token text
903
- outputs=[self._chatbot_component, self._token_usage_component],
954
+ # Outputs: Clear chatbot, reset token text, and update logs
955
+ outputs=[self._chatbot_component, self._token_usage_component, self._log_component],
904
956
  queue=False # Run quickly
905
957
  )
906
958
 
@@ -917,6 +969,12 @@ class GradioCallback:
917
969
  self.assistant_text_responses = []
918
970
  self.token_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
919
971
  self.is_running = False
972
+
973
+ # Clear log stream
974
+ if hasattr(self, 'log_stream'):
975
+ self.log_stream.seek(0)
976
+ self.log_stream.truncate(0)
977
+ self.logger.info("Log stream cleared")
920
978
 
921
979
  # Completely reset the agent state with a new session
922
980
  try:
@@ -965,8 +1023,9 @@ class GradioCallback:
965
1023
  except Exception as e:
966
1024
  self.logger.error(f"Failed to reset TinyAgent completely: {e}")
967
1025
 
968
- # Return cleared UI components: empty chat + fresh token usage
969
- return [], self._get_token_usage_text()
1026
+ # Return cleared UI components: empty chat + fresh token usage + empty logs
1027
+ logs = self.log_stream.getvalue() if hasattr(self, 'log_stream') else ""
1028
+ return [], self._get_token_usage_text(), logs
970
1029
 
971
1030
  def launch(self, agent, title="TinyAgent Chat", description=None, share=False, **kwargs):
972
1031
  """
@@ -1028,21 +1087,31 @@ async def run_example():
1028
1087
  from tinyagent import TinyAgent # Assuming TinyAgent is importable
1029
1088
  from tinyagent.hooks.logging_manager import LoggingManager # Assuming LoggingManager exists
1030
1089
 
1031
- # --- Logging Setup (Simplified) ---
1090
+ # --- Logging Setup (Similar to the example provided) ---
1032
1091
  log_manager = LoggingManager(default_level=logging.INFO)
1033
1092
  log_manager.set_levels({
1034
1093
  'tinyagent.hooks.gradio_callback': logging.DEBUG,
1035
1094
  'tinyagent.tiny_agent': logging.DEBUG,
1036
1095
  'tinyagent.mcp_client': logging.DEBUG,
1096
+ 'tinyagent.code_agent': logging.DEBUG,
1037
1097
  })
1098
+
1099
+ # Console handler for terminal output
1038
1100
  console_handler = logging.StreamHandler(sys.stdout)
1039
1101
  log_manager.configure_handler(
1040
1102
  console_handler,
1041
1103
  format_string='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
1042
1104
  level=logging.DEBUG
1043
1105
  )
1106
+
1107
+ # The Gradio UI will automatically set up its own log handler
1108
+ # through the LoggingManager when we pass it to GradioCallback
1109
+
1110
+ # Get loggers for different components
1044
1111
  ui_logger = log_manager.get_logger('tinyagent.hooks.gradio_callback')
1045
1112
  agent_logger = log_manager.get_logger('tinyagent.tiny_agent')
1113
+ mcp_logger = log_manager.get_logger('tinyagent.mcp_client')
1114
+
1046
1115
  ui_logger.info("--- Starting GradioCallback Example ---")
1047
1116
  # --- End Logging Setup ---
1048
1117
 
@@ -1064,12 +1133,13 @@ async def run_example():
1064
1133
 
1065
1134
  agent.add_tool(get_weather)
1066
1135
 
1067
- # Create the Gradio callback
1136
+ # Create the Gradio callback with LoggingManager integration
1068
1137
  gradio_ui = GradioCallback(
1069
1138
  file_upload_folder=upload_folder,
1070
1139
  show_thinking=True,
1071
1140
  show_tool_calls=True,
1072
- logger=ui_logger # Pass the specific logger
1141
+ logger=ui_logger,
1142
+ log_manager=log_manager # Pass the LoggingManager for comprehensive logging
1073
1143
  )
1074
1144
  agent.add_callback(gradio_ui)
1075
1145
 
@@ -1084,25 +1154,9 @@ async def run_example():
1084
1154
  ui_logger.error(f"Failed to connect to MCP servers: {e}", exc_info=True)
1085
1155
  # Continue without servers - we still have the local get_weather tool
1086
1156
 
1087
- # Create the Gradio app but don't launch it yet
1088
- #app = gradio_ui.create_app(
1089
- # agent,
1090
- # title="TinyAgent Chat Interface",
1091
- # description="Chat with TinyAgent. Try asking: 'Plan a trip to Toronto for 7 days in the next month.'",
1092
- #)
1093
-
1094
- # Configure the queue without extra parameters
1095
- #app.queue()
1096
-
1097
- # Launch the app in a way that doesn't block our event loop
1157
+ # Launch the Gradio interface
1098
1158
  ui_logger.info("Launching Gradio interface...")
1099
1159
  try:
1100
- # Launch without blocking
1101
- #app.launch(
1102
- # share=False,
1103
- # prevent_thread_lock=True, # Critical to not block our event loop
1104
- # show_error=True
1105
- #)
1106
1160
  gradio_ui.launch(
1107
1161
  agent,
1108
1162
  title="TinyAgent Chat Interface",
@@ -1113,9 +1167,19 @@ async def run_example():
1113
1167
  )
1114
1168
  ui_logger.info("Gradio interface launched (non-blocking).")
1115
1169
 
1170
+ # Generate some log messages to demonstrate the log panel
1171
+ # These will appear in both the terminal and the Gradio UI log panel
1172
+ ui_logger.info("UI component initialized successfully")
1173
+ agent_logger.debug("Agent ready to process requests")
1174
+ mcp_logger.info("MCP connection established")
1175
+
1176
+ for i in range(3):
1177
+ ui_logger.info(f"Example log message {i+1} from UI logger")
1178
+ agent_logger.debug(f"Example debug message {i+1} from agent logger")
1179
+ mcp_logger.warning(f"Example warning {i+1} from MCP logger")
1180
+ await asyncio.sleep(1)
1181
+
1116
1182
  # Keep the main event loop running to handle both Gradio and MCP operations
1117
- # This is the key part - we need to keep our main event loop running
1118
- # but also allow it to process both Gradio and MCP client operations
1119
1183
  while True:
1120
1184
  await asyncio.sleep(1) # More efficient than an Event().wait()
1121
1185