tinyagent-py 0.0.11__py3-none-any.whl → 0.0.13__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,
@@ -255,9 +261,31 @@ class TinyCodeAgent:
255
261
  async def run_python(code_lines: List[str], timeout: int = 120) -> str:
256
262
  """Execute Python code using the configured provider."""
257
263
  try:
264
+ # Before execution, ensure provider has the latest user variables
265
+ if self.user_variables:
266
+ self.code_provider.set_user_variables(self.user_variables)
267
+
258
268
  result = await self.code_provider.execute_python(code_lines, timeout)
269
+
270
+ # After execution, update TinyCodeAgent's user_variables from the provider
271
+ # This ensures they stay in sync
272
+ self.user_variables = self.code_provider.get_user_variables()
273
+
259
274
  return str(result)
260
275
  except Exception as e:
276
+ print("!"*100)
277
+ COLOR = {
278
+ "RED": "\033[91m",
279
+ "ENDC": "\033[0m",
280
+ }
281
+ print(f"{COLOR['RED']}{str(e)}{COLOR['ENDC']}")
282
+ print(f"{COLOR['RED']}{traceback.format_exc()}{COLOR['ENDC']}")
283
+ print("!"*100)
284
+
285
+ # Even after an exception, update user_variables from the provider
286
+ # This ensures any variables that were successfully created/modified are preserved
287
+ self.user_variables = self.code_provider.get_user_variables()
288
+
261
289
  return f"Error executing code: {str(e)}"
262
290
 
263
291
  self.agent.add_tool(run_python)
@@ -441,6 +469,69 @@ class TinyCodeAgent:
441
469
  """
442
470
  return self.pip_packages.copy()
443
471
 
472
+ def add_authorized_imports(self, imports: List[str]):
473
+ """
474
+ Add additional authorized imports to the execution environment.
475
+
476
+ Args:
477
+ imports: List of import names to authorize
478
+ """
479
+ self.authorized_imports.extend(imports)
480
+ self.authorized_imports = list(set(self.authorized_imports)) # Remove duplicates
481
+
482
+ # Update the provider with the new authorized imports
483
+ # This requires recreating the provider
484
+ print("⚠️ Warning: Adding authorized imports after initialization requires recreating the Modal environment.")
485
+ print(" For better performance, set authorized_imports during TinyCodeAgent initialization.")
486
+
487
+ # Recreate the provider with new authorized imports
488
+ self.code_provider = self._create_provider(self.provider, self.provider_config)
489
+
490
+ # Re-set user variables if they exist
491
+ if self.user_variables:
492
+ self.code_provider.set_user_variables(self.user_variables)
493
+
494
+ # Rebuild system prompt to include new authorized imports
495
+ self.system_prompt = self._build_system_prompt()
496
+ # Update the agent's system prompt
497
+ self.agent.system_prompt = self.system_prompt
498
+
499
+ def get_authorized_imports(self) -> List[str]:
500
+ """
501
+ Get a copy of current authorized imports.
502
+
503
+ Returns:
504
+ List of authorized imports
505
+ """
506
+ return self.authorized_imports.copy()
507
+
508
+ def remove_authorized_import(self, import_name: str):
509
+ """
510
+ Remove an authorized import.
511
+
512
+ Args:
513
+ import_name: Import name to remove
514
+ """
515
+ if import_name in self.authorized_imports:
516
+ self.authorized_imports.remove(import_name)
517
+
518
+ # Update the provider with the new authorized imports
519
+ # This requires recreating the provider
520
+ print("⚠️ Warning: Removing authorized imports after initialization requires recreating the Modal environment.")
521
+ print(" For better performance, set authorized_imports during TinyCodeAgent initialization.")
522
+
523
+ # Recreate the provider with updated authorized imports
524
+ self.code_provider = self._create_provider(self.provider, self.provider_config)
525
+
526
+ # Re-set user variables if they exist
527
+ if self.user_variables:
528
+ self.code_provider.set_user_variables(self.user_variables)
529
+
530
+ # Rebuild system prompt to reflect updated authorized imports
531
+ self.system_prompt = self._build_system_prompt()
532
+ # Update the agent's system prompt
533
+ self.agent.system_prompt = self.system_prompt
534
+
444
535
  async def close(self):
445
536
  """Clean up resources."""
446
537
  await self.code_provider.cleanup()
@@ -498,6 +589,7 @@ async def run_example():
498
589
  user_variables={
499
590
  "sample_data": [1, 2, 3, 4, 5, 10, 15, 20]
500
591
  },
592
+ authorized_imports=["tinyagent", "gradio", "requests", "numpy", "pandas"], # Explicitly specify authorized imports
501
593
  local_execution=False # Remote execution via Modal (default)
502
594
  )
503
595
 
@@ -524,6 +616,7 @@ async def run_example():
524
616
  user_variables={
525
617
  "sample_data": [1, 2, 3, 4, 5, 10, 15, 20]
526
618
  },
619
+ authorized_imports=["tinyagent", "gradio", "requests"], # More restricted imports for local execution
527
620
  local_execution=True # Local execution
528
621
  )
529
622
 
@@ -550,6 +643,18 @@ async def run_example():
550
643
  agent_remote.add_code_tool(validator)
551
644
  agent_local.add_code_tool(validator)
552
645
 
646
+ # Demonstrate adding authorized imports dynamically
647
+ print("\n" + "="*80)
648
+ print("🔧 Testing with dynamically added authorized imports")
649
+ agent_remote.add_authorized_imports(["matplotlib", "seaborn"])
650
+
651
+ # Test with visualization libraries
652
+ viz_prompt = "Create a simple plot of the sample_data and save it as a base64 encoded image string."
653
+
654
+ response_viz = await agent_remote.run(viz_prompt)
655
+ print("Remote Agent Visualization Response:")
656
+ print(response_viz)
657
+
553
658
  print("\n" + "="*80)
554
659
  print("🔧 Testing with dynamically added tools")
555
660
 
@@ -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,25 +97,62 @@ 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
 
78
- tree = ast.parse(code, mode="exec")
79
- compiled = compile(tree, filename="<ast>", mode="exec")
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
119
+ try:
120
+ tree = ast.parse(code, mode="exec")
121
+ compiled = compile(tree, filename="<ast>", mode="exec")
122
+ except SyntaxError as e:
123
+ # Return syntax error without executing
124
+ return {
125
+ "printed_output": "",
126
+ "return_value": None,
127
+ "stderr": "",
128
+ "error_traceback": f"Syntax error: {str(e)}",
129
+ "updated_globals": updated_globals,
130
+ "updated_locals": updated_locals
131
+ }
132
+
80
133
  stdout_buf = io.StringIO()
81
- stderr_buf = io.StringIO()
82
-
83
- # Execute with stdout+stderr capture and exception handling
134
+ stderr_buf = io.StringIO()
135
+ # Execute with exception handling
84
136
  error_traceback = None
85
137
  output = None
86
138
 
139
+ # Merge all variables into globals to avoid scoping issues with generator expressions
140
+ # When exec() is called with both globals and locals, generator expressions can't
141
+ # access local variables. By using only globals, everything runs in global scope.
142
+ merged_globals = updated_globals.copy()
143
+ merged_globals.update(updated_locals)
144
+
87
145
  with contextlib.redirect_stdout(stdout_buf), contextlib.redirect_stderr(stderr_buf):
88
146
  try:
89
- # Merge all variables into globals to avoid scoping issues with generator expressions
90
- # When exec() is called with both globals and locals, generator expressions can't
91
- # access local variables. By using only globals, everything runs in global scope.
92
- merged_globals = updated_globals.copy()
93
- merged_globals.update(updated_locals)
147
+ # Add 'exec' to authorized_functions for internal use
148
+ internal_authorized_functions = ['exec','eval']
149
+ if authorized_functions is not None and not isinstance(authorized_functions, bool):
150
+ internal_authorized_functions.extend(authorized_functions)
94
151
 
95
152
  # Execute with only globals - this fixes generator expression scoping issues
96
- output = exec(code, merged_globals)
153
+ # Use the function_safety_context to block dangerous functions during execution
154
+ with function_safety_context(authorized_functions=internal_authorized_functions, trusted_code=trusted_code):
155
+ output = exec(compiled, merged_globals)
97
156
 
98
157
  # Update both dictionaries with any new variables created during execution
99
158
  for key, value in merged_globals.items():
@@ -105,7 +164,21 @@ def _run_python(code: str, globals_dict: Dict[str, Any] = None, locals_dict: Dic
105
164
  except Exception:
106
165
  # Capture the full traceback as a string
107
166
  error_traceback = traceback.format_exc()
167
+
168
+ # CRITICAL FIX: Even when an exception occurs, we need to update the globals and locals
169
+ # with any variables that were successfully created/modified before the exception
170
+ for key, value in merged_globals.items():
171
+ # Skip special variables and modules
172
+ if key.startswith('__') or key in ['builtins', 'traceback', 'contextlib', 'io', 'ast', 'sys']:
173
+ continue
174
+
175
+ # Update both dictionaries with the current state
176
+ if key in updated_locals or key not in updated_globals:
177
+ updated_locals[key] = value
178
+ updated_globals[key] = value
108
179
 
180
+ # Join all captured output
181
+ #printed_output = ''.join(output_buffer)
109
182
  printed_output = stdout_buf.getvalue()
110
183
  stderr_output = stderr_buf.getvalue()
111
184
  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
  """
@@ -809,8 +845,9 @@ class GradioCallback:
809
845
 
810
846
  # Footer
811
847
  gr.Markdown(
812
- "<div style='text-align: center; margin-top: 20px;'>"
813
- "Powered by <a href='https://github.com/askbudi/tinyagent' target='_blank'>TinyAgent</a>"
848
+ "<div style='text-align: center; margin-top: 20px;'>"
849
+ "Built with ❤️ by <a href='https://github.com/askbudi/tinyagent' target='_blank'>TinyAgent</a>"
850
+ "<br>Start building your own AI agents with TinyAgent"
814
851
  "</div>"
815
852
  )
816
853
 
@@ -839,6 +876,22 @@ class GradioCallback:
839
876
  # Clear button
840
877
  clear_btn = gr.Button("Clear Conversation")
841
878
 
879
+ # Log accordion - similar to the example provided
880
+ with gr.Accordion("Agent Logs", open=False) as log_accordion:
881
+ self._log_component = gr.Code(
882
+ label="Live Logs",
883
+ lines=15,
884
+ interactive=False,
885
+ value=self.log_stream.getvalue()
886
+ )
887
+ refresh_logs_btn = gr.Button("🔄 Refresh Logs")
888
+ refresh_logs_btn.click(
889
+ fn=lambda: self.log_stream.getvalue(),
890
+ inputs=None,
891
+ outputs=[self._log_component],
892
+ queue=False
893
+ )
894
+
842
895
  # Store processed input temporarily between steps
843
896
  processed_input_state = gr.State("")
844
897
 
@@ -859,7 +912,7 @@ class GradioCallback:
859
912
  # 3. Run the main interaction loop (this yields updates)
860
913
  fn=self.interact_with_agent,
861
914
  inputs=[processed_input_state, self._chatbot_component],
862
- outputs=[self._chatbot_component, self._token_usage_component], # Update chat and tokens
915
+ outputs=[self._chatbot_component, self._token_usage_component, self._log_component], # Update chat, tokens, and logs
863
916
  queue=True # Explicitly enable queue for this async generator
864
917
  ).then(
865
918
  # 4. Re-enable the button after interaction finishes
@@ -885,7 +938,7 @@ class GradioCallback:
885
938
  # 3. Run the main interaction loop (this yields updates)
886
939
  fn=self.interact_with_agent,
887
940
  inputs=[processed_input_state, self._chatbot_component],
888
- outputs=[self._chatbot_component, self._token_usage_component], # Update chat and tokens
941
+ outputs=[self._chatbot_component, self._token_usage_component, self._log_component], # Update chat, tokens, and logs
889
942
  queue=True # Explicitly enable queue for this async generator
890
943
  ).then(
891
944
  # 4. Re-enable the button after interaction finishes
@@ -899,8 +952,8 @@ class GradioCallback:
899
952
  clear_btn.click(
900
953
  fn=self.clear_conversation,
901
954
  inputs=None, # No inputs needed
902
- # Outputs: Clear chatbot and reset token text
903
- outputs=[self._chatbot_component, self._token_usage_component],
955
+ # Outputs: Clear chatbot, reset token text, and update logs
956
+ outputs=[self._chatbot_component, self._token_usage_component, self._log_component],
904
957
  queue=False # Run quickly
905
958
  )
906
959
 
@@ -917,6 +970,12 @@ class GradioCallback:
917
970
  self.assistant_text_responses = []
918
971
  self.token_usage = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
919
972
  self.is_running = False
973
+
974
+ # Clear log stream
975
+ if hasattr(self, 'log_stream'):
976
+ self.log_stream.seek(0)
977
+ self.log_stream.truncate(0)
978
+ self.logger.info("Log stream cleared")
920
979
 
921
980
  # Completely reset the agent state with a new session
922
981
  try:
@@ -965,8 +1024,9 @@ class GradioCallback:
965
1024
  except Exception as e:
966
1025
  self.logger.error(f"Failed to reset TinyAgent completely: {e}")
967
1026
 
968
- # Return cleared UI components: empty chat + fresh token usage
969
- return [], self._get_token_usage_text()
1027
+ # Return cleared UI components: empty chat + fresh token usage + empty logs
1028
+ logs = self.log_stream.getvalue() if hasattr(self, 'log_stream') else ""
1029
+ return [], self._get_token_usage_text(), logs
970
1030
 
971
1031
  def launch(self, agent, title="TinyAgent Chat", description=None, share=False, **kwargs):
972
1032
  """
@@ -1028,21 +1088,31 @@ async def run_example():
1028
1088
  from tinyagent import TinyAgent # Assuming TinyAgent is importable
1029
1089
  from tinyagent.hooks.logging_manager import LoggingManager # Assuming LoggingManager exists
1030
1090
 
1031
- # --- Logging Setup (Simplified) ---
1091
+ # --- Logging Setup (Similar to the example provided) ---
1032
1092
  log_manager = LoggingManager(default_level=logging.INFO)
1033
1093
  log_manager.set_levels({
1034
1094
  'tinyagent.hooks.gradio_callback': logging.DEBUG,
1035
1095
  'tinyagent.tiny_agent': logging.DEBUG,
1036
1096
  'tinyagent.mcp_client': logging.DEBUG,
1097
+ 'tinyagent.code_agent': logging.DEBUG,
1037
1098
  })
1099
+
1100
+ # Console handler for terminal output
1038
1101
  console_handler = logging.StreamHandler(sys.stdout)
1039
1102
  log_manager.configure_handler(
1040
1103
  console_handler,
1041
1104
  format_string='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
1042
1105
  level=logging.DEBUG
1043
1106
  )
1107
+
1108
+ # The Gradio UI will automatically set up its own log handler
1109
+ # through the LoggingManager when we pass it to GradioCallback
1110
+
1111
+ # Get loggers for different components
1044
1112
  ui_logger = log_manager.get_logger('tinyagent.hooks.gradio_callback')
1045
1113
  agent_logger = log_manager.get_logger('tinyagent.tiny_agent')
1114
+ mcp_logger = log_manager.get_logger('tinyagent.mcp_client')
1115
+
1046
1116
  ui_logger.info("--- Starting GradioCallback Example ---")
1047
1117
  # --- End Logging Setup ---
1048
1118
 
@@ -1064,12 +1134,13 @@ async def run_example():
1064
1134
 
1065
1135
  agent.add_tool(get_weather)
1066
1136
 
1067
- # Create the Gradio callback
1137
+ # Create the Gradio callback with LoggingManager integration
1068
1138
  gradio_ui = GradioCallback(
1069
1139
  file_upload_folder=upload_folder,
1070
1140
  show_thinking=True,
1071
1141
  show_tool_calls=True,
1072
- logger=ui_logger # Pass the specific logger
1142
+ logger=ui_logger,
1143
+ log_manager=log_manager # Pass the LoggingManager for comprehensive logging
1073
1144
  )
1074
1145
  agent.add_callback(gradio_ui)
1075
1146
 
@@ -1084,25 +1155,9 @@ async def run_example():
1084
1155
  ui_logger.error(f"Failed to connect to MCP servers: {e}", exc_info=True)
1085
1156
  # Continue without servers - we still have the local get_weather tool
1086
1157
 
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
1158
+ # Launch the Gradio interface
1098
1159
  ui_logger.info("Launching Gradio interface...")
1099
1160
  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
1161
  gradio_ui.launch(
1107
1162
  agent,
1108
1163
  title="TinyAgent Chat Interface",
@@ -1113,9 +1168,19 @@ async def run_example():
1113
1168
  )
1114
1169
  ui_logger.info("Gradio interface launched (non-blocking).")
1115
1170
 
1171
+ # Generate some log messages to demonstrate the log panel
1172
+ # These will appear in both the terminal and the Gradio UI log panel
1173
+ ui_logger.info("UI component initialized successfully")
1174
+ agent_logger.debug("Agent ready to process requests")
1175
+ mcp_logger.info("MCP connection established")
1176
+
1177
+ for i in range(3):
1178
+ ui_logger.info(f"Example log message {i+1} from UI logger")
1179
+ agent_logger.debug(f"Example debug message {i+1} from agent logger")
1180
+ mcp_logger.warning(f"Example warning {i+1} from MCP logger")
1181
+ await asyncio.sleep(1)
1182
+
1116
1183
  # 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
1184
  while True:
1120
1185
  await asyncio.sleep(1) # More efficient than an Event().wait()
1121
1186
 
tinyagent/tiny_agent.py CHANGED
@@ -725,13 +725,10 @@ class TinyAgent:
725
725
  next_turn_should_call_tools = False
726
726
  else:
727
727
  # No tool calls in this message
728
- if num_turns > 0:
729
- #if next_turn_should_call_tools and num_turns > 0:
730
- # If we expected tool calls but didn't get any, we're done
731
- await self._run_callbacks("agent_end", result=assistant_msg["content"] or "")
732
- return assistant_msg["content"] or ""
733
-
734
- next_turn_should_call_tools = True
728
+ # If the model provides a direct answer without tool calls, we should return it
729
+ # This handles the case where the LLM gives a direct answer without using tools
730
+ await self._run_callbacks("agent_end", result=assistant_msg["content"] or "")
731
+ return assistant_msg["content"] or ""
735
732
 
736
733
  num_turns += 1
737
734
  if num_turns >= max_turns:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinyagent-py
3
- Version: 0.0.11
3
+ Version: 0.0.13
4
4
  Summary: TinyAgent with MCP Client, Code Agent (Thinking, Planning, and Executing in Python), and Extendable Hooks, Tiny but powerful
5
5
  Author-email: Mahdi Golchin <golchin@askdev.ai>
6
6
  Project-URL: Homepage, https://github.com/askbudi/tinyagent
@@ -60,6 +60,16 @@ Inspired by:
60
60
  ## Quick Links
61
61
  - [Build your own Tiny Agent](https://askdev.ai/github/askbudi/tinyagent)
62
62
 
63
+
64
+ ## Live Projects using TinyAgent (🔥)
65
+ - [AskDev.AI](https://askdev.ai) - Understand, chat, and summarize codebase of any project on GitHub.
66
+ - [HackBuddy AI](https://huggingface.co/spaces/ask-dev/HackBuddyAI) - A Hackathon Assistant Agent, built with TinyCodeAgent and Gradio. Match invdividuals to teams based on their skills, interests and organizer preferences.
67
+
68
+ - [TinyCodeAgent Demo](https://huggingface.co/spaces/ask-dev/TinyCodeAgent) - A playground for TinyCodeAgent, built with tinyagent, Gradio and Modal.com
69
+
70
+ ** Building something with TinyAgent? Let us know and I'll add it here!**
71
+
72
+
63
73
  ## Overview
64
74
  This is a tiny agent framework that uses MCP and LiteLLM to interact with language models. You have full control over the agent, you can add any tools you like from MCP and extend the agent using its event system.
65
75