|
import asyncio |
|
import sys |
|
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: |
|
|
|
cloudpickle.dumps(val) |
|
except Exception: |
|
|
|
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 |
|
|
|
|
|
updated_globals = globals_dict.copy() |
|
updated_locals = locals_dict.copy() |
|
|
|
|
|
|
|
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() |
|
|
|
|
|
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: |
|
|
|
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}" |
|
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>!!!!!!!!!!!!!!!!!!!!!!!!!") |
|
|
|
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 |
|
from tinyagent.hooks.logging_manager import LoggingManager |
|
from tinyagent.hooks.gradio_callback import GradioCallback |
|
import logging |
|
|
|
|
|
|
|
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 ---") |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
upload_folder = tempfile.mkdtemp(prefix="gradio_uploads_") |
|
ui_logger.info(f"Created temporary upload folder: {upload_folder}") |
|
|
|
|
|
loop = asyncio.get_event_loop() |
|
ui_logger.debug(f"Using event loop: {loop}") |
|
|
|
|
|
|
|
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, |
|
|
|
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) |
|
|
|
|
|
|
|
gradio_ui = GradioCallback( |
|
file_upload_folder=upload_folder, |
|
show_thinking=True, |
|
show_tool_calls=True, |
|
logger=ui_logger |
|
) |
|
agent.add_callback(gradio_ui) |
|
|
|
|
|
try: |
|
ui_logger.info("Connecting to MCP servers...") |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui_logger.info("Launching Gradio interface...") |
|
try: |
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
show_error=True, |
|
mcp_server=True, |
|
) |
|
ui_logger.info("Gradio interface launched (non-blocking).") |
|
|
|
|
|
|
|
|
|
while True: |
|
await asyncio.sleep(1) |
|
|
|
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: |
|
|
|
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()) |