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())