File size: 15,234 Bytes
ebb6a31 60e4781 ebb6a31 0395e65 ebb6a31 c9927fa ebb6a31 60e4781 33b6737 ebb6a31 c9927fa 2d59587 ebb6a31 33b6737 ebb6a31 2d59587 ebb6a31 7a78fe5 ebb6a31 0395e65 ebb6a31 0395e65 ebb6a31 0395e65 7a78fe5 ebb6a31 0395e65 7a78fe5 ebb6a31 2d59587 7a78fe5 ebb6a31 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 |
import asyncio
import sys # Add sys import
from tinyagent import tool
from textwrap import dedent
from typing import Optional, List, Dict, Any,Union
from tinyagent.hooks.logging_manager import LoggingManager
import modal
import cloudpickle
def clean_response(resp):
return {k:v for k,v in resp.items() if k in ['printed_output','return_value','stderr','error_traceback']}
def make_session_blob(ns: dict) -> bytes:
clean = {}
for name, val in ns.items():
try:
# Try serializing just this one object
cloudpickle.dumps(val)
except Exception:
# drop anything that fails
continue
else:
clean[name] = val
return cloudpickle.dumps(clean)
def _run_python(code: str,globals_dict:Dict[str,Any]={},locals_dict:Dict[str,Any]={}):
import contextlib
import traceback
import io
import ast
# Make copies to avoid mutating the original parameters
updated_globals = globals_dict.copy()
updated_locals = locals_dict.copy()
# Pre-import essential modules into the global namespace
# This ensures they're available for imports inside functions
essential_modules = ['requests', 'json', 'os', 'sys', 'time', 'datetime', 're', 'random', 'math']
for module_name in essential_modules:
try:
module = __import__(module_name)
updated_globals[module_name] = module
print(f"✓ {module_name} module loaded successfully")
except ImportError:
print(f"⚠️ Warning: {module_name} module not available")
tree = ast.parse(code, mode="exec")
compiled = compile(tree, filename="<ast>", mode="exec")
stdout_buf = io.StringIO()
stderr_buf = io.StringIO()
# --- 4. Execute with stdout+stderr capture and exception handling ---
error_traceback = None
output = None
with contextlib.redirect_stdout(stdout_buf), contextlib.redirect_stderr(stderr_buf):
try:
output = exec(code, updated_globals, updated_locals)
except Exception:
# Capture the full traceback as a string
error_traceback = traceback.format_exc()
printed_output = stdout_buf.getvalue()
stderr_output = stderr_buf.getvalue()
error_traceback_output = error_traceback
return {
"printed_output": printed_output,
"return_value": output,
"stderr": stderr_output,
"error_traceback": error_traceback_output,
"updated_globals": updated_globals,
"updated_locals": updated_locals
}
class PythonCodeInterpreter:
executed_default_codes = False
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}" # Automatically detect Python version
sandbox_name = "tinycodeagent-sandbox"
app = None
_app_run_python = None
_globals_dict = {}
_locals_dict = {}
def __init__(self,log_manager: LoggingManager,
default_python_codes:Optional[List[str]]=[],
code_tools:List[Dict[str,Any]]=[],
pip_packages:List[str]=[],
modal_secrets:Dict[str,Union[str,None]]={},
lazy_init:bool=True,
**kwargs):
self.log_manager = log_manager
self.code_tools = code_tools
self._globals_dict.update(**kwargs.get("globals_dict",{}))
self._locals_dict.update(**kwargs.get("locals_dict",{}))
self.default_python_codes = default_python_codes
self.modal_secrets = modal.Secret.from_dict(modal_secrets)
self.pip_packages = list(set(["cloudpickle","requests","tinyagent-py[all]==0.0.8",
"gradio",
"arize-phoenix-otel",]+pip_packages))
self.lazy_init = lazy_init
self.create_app(self.modal_secrets,self.pip_packages,self.code_tools)
def create_app(self,modal_secrets:Dict[str,Union[str,None]],pip_packages:List[str]=[],code_tools:List[Dict[str,Any]]=[]):
agent_image = modal.Image.debian_slim(python_version=self.PYTHON_VERSION).pip_install(
*pip_packages
)
self.app = modal.App(
name=self.sandbox_name,
image=agent_image,
secrets=[modal_secrets]
)
self._app_run_python = self.app.function()(_run_python)
self.add_tools(code_tools)
return self.app
def add_tools(self,tools):
tools_str_list = ["import cloudpickle"]
tools_str_list.append("###########<tools>###########\n")
for tool in tools:
tools_str_list.append(f"globals()['{tool._tool_metadata['name']}'] = cloudpickle.loads( {cloudpickle.dumps(tool)})")
tools_str_list.append("\n\n")
tools_str_list.append("###########</tools>###########\n")
tools_str_list.append("\n\n")
self.default_python_codes.extend(tools_str_list)
def _python_executor(self,code: str,globals_dict:Dict[str,Any]={},locals_dict:Dict[str,Any]={}):
with self.app.run():
if self.executed_default_codes:
print("✔️ default codes already executed")
full_code = code
else:
full_code = "\n".join(self.default_python_codes)+ "\n\n"+(code)
self.executed_default_codes = True
return self._app_run_python.remote(full_code,globals_dict,locals_dict)
@tool(name="run_python",description=dedent("""
This tool receive python code, and execute it,
During each intermediate step, you can use 'print()' to save whatever important information you will then need.
These print outputs will then appear in the 'Observation:' field, which will be available as input for the next step.
Args:
code_lines: list[str]: The python code to execute, it should be a valid python code, and it should be able to run without any errors.
Your code should be include all the steps neccesary for successful run, cover edge cases, error handling
In case of an error, you will see the error, so get the most out of print function to debug your code.
Each line of code should be an independent line of code, and it should be able to run without any errors.
Returns:
Status of code execution or error message.
"""))
async def run_python(self,code_lines:list[str],timeout:int=120) -> str:
if type(code_lines) == str:
code_lines = [code_lines]
code = code_lines
full_code = "\n".join(code)
print("##"*50)
print("#########################code#########################")
print(full_code)
print("##"*50)
response = self._python_executor(full_code,self._globals_dict,self._locals_dict)
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!<response>!!!!!!!!!!!!!!!!!!!!!!!!!")
# Update the instance globals and locals with the execution results
self._globals_dict = cloudpickle.loads(make_session_blob(response["updated_globals"]))
self._locals_dict = cloudpickle.loads(make_session_blob(response["updated_locals"]))
print("#########################<printed_output>#########################")
print(response["printed_output"])
print("#########################</printed_output>#########################")
print("#########################<return_value>#########################")
print(response["return_value"])
print("#########################</return_value>#########################")
print("#########################<stderr>#########################")
print(response["stderr"])
print("#########################</stderr>#########################")
print("#########################<traceback>#########################")
print(response["error_traceback"])
print("#########################</traceback>#########################")
return clean_response(response)
weather_global = '-'
traffic_global = '-'
@tool(name="get_weather",description="Get the weather for a given city.")
def get_weather(city: str)->str:
"""Get the weather for a given city.
Args:
city: The city to get the weather for
Returns:
The weather for the given city
"""
import random
global weather_global
output = f"Last time weather was checked was {weather_global}"
weather_global = random.choice(['sunny','cloudy','rainy','snowy'])
output += f"\n\nThe weather in {city} is now {weather_global}"
return output
@tool(name="get_traffic",description="Get the traffic for a given city.")
def get_traffic(city: str)->str:
"""Get the traffic for a given city.
Args:
city: The city to get the traffic for
Returns:
The traffic for the given city
"""
import random
global traffic_global
output = f"Last time traffic was checked was {traffic_global}"
traffic_global = random.choice(['light','moderate','heavy','blocked'])
output += f"\n\nThe traffic in {city} is now {traffic_global}"
return output
async def run_example():
"""Example usage of GradioCallback with TinyAgent."""
import os
import sys
import tempfile
import shutil
import asyncio
from tinyagent import TinyAgent # Assuming TinyAgent is importable
from tinyagent.hooks.logging_manager import LoggingManager # Assuming LoggingManager exists
from tinyagent.hooks.gradio_callback import GradioCallback
import logging
# --- Logging Setup (Simplified) ---
log_manager = LoggingManager(default_level=logging.INFO)
log_manager.set_levels({
'tinyagent.hooks.gradio_callback': logging.DEBUG,
'tinyagent.tiny_agent': logging.DEBUG,
'tinyagent.mcp_client': logging.DEBUG,
})
console_handler = logging.StreamHandler(sys.stdout)
log_manager.configure_handler(
console_handler,
format_string='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.DEBUG
)
ui_logger = log_manager.get_logger('tinyagent.hooks.gradio_callback')
agent_logger = log_manager.get_logger('tinyagent.tiny_agent')
ui_logger.info("--- Starting GradioCallback Example ---")
# --- End Logging Setup ---
#api_key = os.environ.get("NEBIUS_API_KEY")
#model = "openai/Qwen/Qwen3-235B-A22B"
#model = "openai/Qwen/Qwen3-30B-A3B-fast"
model = "gpt-4.1-mini"
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
ui_logger.error("NEBIUS_API_KEY environment variable not set.")
return
# Create a temporary folder for file uploads
upload_folder = tempfile.mkdtemp(prefix="gradio_uploads_")
ui_logger.info(f"Created temporary upload folder: {upload_folder}")
# Ensure we're using a single event loop for everything
loop = asyncio.get_event_loop()
ui_logger.debug(f"Using event loop: {loop}")
# Initialize the agent
from helper import translate_tool_for_code_agent,load_template,render_system_prompt,prompt_code_example,prompt_qwen_helper
tools = [get_weather,get_traffic]
tools_meta_data = {}
for tool in tools:
metadata = translate_tool_for_code_agent(tool)
tools_meta_data[metadata["name"]] = metadata
template_str = load_template("./prompts/code_agent.yaml")
system_prompt = render_system_prompt(template_str, tools_meta_data, {}, ["tinyagent","gradio","requests","asyncio"]) + prompt_code_example + prompt_qwen_helper
agent = TinyAgent(model=model, api_key=api_key,
#model_kwargs=dict(base_url="https://api.studio.nebius.com/v1/"),
logger=agent_logger,
system_prompt=system_prompt,
)
python_interpreter = PythonCodeInterpreter(log_manager=log_manager,code_tools=tools,pip_packages=["tinyagent-py[all]","requests","cloudpickle"],
default_python_codes=["import random","import requests","import cloudpickle","import tempfile","import shutil","import asyncio","import logging","import time"])
agent.add_tool(python_interpreter.run_python)
# Create the Gradio callback
gradio_ui = GradioCallback(
file_upload_folder=upload_folder,
show_thinking=True,
show_tool_calls=True,
logger=ui_logger # Pass the specific logger
)
agent.add_callback(gradio_ui)
# Connect to MCP servers
try:
ui_logger.info("Connecting to MCP servers...")
# Use standard MCP servers as per contribution guide
#await agent.connect_to_server("npx",["-y","@openbnb/mcp-server-airbnb","--ignore-robots-txt"])
await agent.connect_to_server("npx", ["-y", "@modelcontextprotocol/server-sequential-thinking"])
ui_logger.info("Connected to MCP servers.")
except Exception as e:
ui_logger.error(f"Failed to connect to MCP servers: {e}", exc_info=True)
# Continue without servers - we still have the local get_weather tool
# Create the Gradio app but don't launch it yet
#app = gradio_ui.create_app(
# agent,
# title="TinyAgent Chat Interface",
# description="Chat with TinyAgent. Try asking: 'Plan a trip to Toronto for 7 days in the next month.'",
#)
# Configure the queue without extra parameters
#app.queue()
# Launch the app in a way that doesn't block our event loop
ui_logger.info("Launching Gradio interface...")
try:
# Launch without blocking
#app.launch(
# share=False,
# prevent_thread_lock=True, # Critical to not block our event loop
# show_error=True
#)
gradio_ui.launch(
agent,
title="TinyCodeAgent Chat Interface",
description="Chat with TinyAgent. Try asking: 'I need to know the weather and traffic in Toronto, Montreal, New York, Paris and San Francisco.'",
share=False,
prevent_thread_lock=True, # Critical to not block our event loop
show_error=True,
mcp_server=True,
)
ui_logger.info("Gradio interface launched (non-blocking).")
# Keep the main event loop running to handle both Gradio and MCP operations
# This is the key part - we need to keep our main event loop running
# but also allow it to process both Gradio and MCP client operations
while True:
await asyncio.sleep(1) # More efficient than an Event().wait()
except KeyboardInterrupt:
ui_logger.info("Received keyboard interrupt, shutting down...")
except Exception as e:
ui_logger.error(f"Failed to launch or run Gradio app: {e}", exc_info=True)
finally:
# Clean up
ui_logger.info("Cleaning up resources...")
if os.path.exists(upload_folder):
ui_logger.info(f"Removing temporary upload folder: {upload_folder}")
shutil.rmtree(upload_folder)
await agent.close()
ui_logger.info("--- GradioCallback Example Finished ---")
if __name__ == "__main__":
asyncio.run(run_example()) |