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.
- tinyagent/code_agent/modal_sandbox.py +3 -1
- tinyagent/code_agent/providers/base.py +60 -5
- tinyagent/code_agent/providers/modal_provider.py +61 -18
- tinyagent/code_agent/safety.py +546 -0
- tinyagent/code_agent/tiny_code_agent.py +105 -0
- tinyagent/code_agent/utils.py +90 -17
- tinyagent/hooks/gradio_callback.py +100 -35
- tinyagent/tiny_agent.py +4 -7
- {tinyagent_py-0.0.11.dist-info → tinyagent_py-0.0.13.dist-info}/METADATA +11 -1
- {tinyagent_py-0.0.11.dist-info → tinyagent_py-0.0.13.dist-info}/RECORD +13 -12
- {tinyagent_py-0.0.11.dist-info → tinyagent_py-0.0.13.dist-info}/WHEEL +0 -0
- {tinyagent_py-0.0.11.dist-info → tinyagent_py-0.0.13.dist-info}/licenses/LICENSE +0 -0
- {tinyagent_py-0.0.11.dist-info → tinyagent_py-0.0.13.dist-info}/top_level.txt +0 -0
@@ -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
|
|
tinyagent/code_agent/utils.py
CHANGED
@@ -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(
|
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
|
-
#
|
67
|
-
#
|
68
|
-
|
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
|
-
|
79
|
-
|
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
|
-
#
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
813
|
-
"
|
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
|
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
|
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
|
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
|
-
|
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 (
|
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
|
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
|
-
#
|
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
|
-
|
729
|
-
#
|
730
|
-
|
731
|
-
|
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.
|
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
|
|