ag2 0.9.5__py3-none-any.whl → 0.9.7__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.

Potentially problematic release.


This version of ag2 might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ag2
3
- Version: 0.9.5
3
+ Version: 0.9.7
4
4
  Summary: A programming framework for agentic AI
5
5
  Project-URL: Homepage, https://ag2.ai/
6
6
  Project-URL: Documentation, https://docs.ag2.ai
@@ -8,13 +8,13 @@ autogen/function_utils.py,sha256=YnzwNFA49Jbbe4riAY1sinYcKphg5lrHFCXx0POdYbw,481
8
8
  autogen/graph_utils.py,sha256=2dfGUHZCCF629vh0vMK9WMXIX-Zi-if9NbdC0KFcuw4,7904
9
9
  autogen/import_utils.py,sha256=byPlexRvu1bkGzEAtwwtJ2SQIm1IzModK9B5ONGkqJw,17634
10
10
  autogen/json_utils.py,sha256=_61k__3SS1-ffE2K5Mm9ZGHQJBRUbOL9kHGQGTAWRZk,1378
11
- autogen/llm_config.py,sha256=eFWSN9yR3w2R7d2CdDQ5ZpU2tUy8xBzEwGuq5K6QZLs,14172
11
+ autogen/llm_config.py,sha256=lQPV3zXbLKL_7rJHi1fJAJn1NpNK7wZtqGUxHeapIZQ,14437
12
12
  autogen/math_utils.py,sha256=Ew9-I5chny9eAOnphGxKf6XCeW6Pepz5S-5touWrbYU,9546
13
13
  autogen/retrieve_utils.py,sha256=R3Yp5d8dH4o9ayLZrGn4rCjIaY4glOHIiyQjwClmdi8,20087
14
14
  autogen/runtime_logging.py,sha256=yCmZODvwqYR91m8lX3Q4SoPcY-DK48NF4m56CP6Om3c,4692
15
15
  autogen/token_count_utils.py,sha256=n4wTFVNHwrfjZkrErFr8kNig2K-YCGgMLWsjDRS9D6g,10797
16
16
  autogen/types.py,sha256=qu-7eywhakW2AxQ5lYisLLeIg45UoOW-b3ErIuyRTuw,1000
17
- autogen/version.py,sha256=THVipdL6_RedIuOtoczdX7_iJxpvPiU7gkSD-mvqhIo,193
17
+ autogen/version.py,sha256=aw3BBOt8HZBovklvFoq90ok-Yb3gkEvbuckRDabaBTs,193
18
18
  autogen/_website/__init__.py,sha256=c8B9TpO07x9neD0zsJWj6AaEdlcP-WvxrvVOGWLtamk,143
19
19
  autogen/_website/generate_api_references.py,sha256=yKqyeSP_NE27wwLYWsZbTYRceEoxzNxPXqn6vsIzEvk,14789
20
20
  autogen/_website/generate_mkdocs.py,sha256=TkmLnUDv1Ms5cGClXPmenA8nxmwg4kR0E-FHCVjw_og,43246
@@ -25,7 +25,7 @@ autogen/agentchat/__init__.py,sha256=d5jPpXeavynP9eh5uMIgJKbK3-3tywJBa6l3L9X4l34
25
25
  autogen/agentchat/agent.py,sha256=HePNJ5BXJTcZtaD2a8CoTeHAoLUUy3scM6Ihm-NsSWk,5828
26
26
  autogen/agentchat/assistant_agent.py,sha256=XTJvD66r4qYkdNAJJLr1CC-wTYFJWvhmD5_G0WbbX2I,5741
27
27
  autogen/agentchat/chat.py,sha256=6Gx2t1-Xa8kP6ZoUihHBNGOqNlrGhhqLPKrckL0n-RI,14003
28
- autogen/agentchat/conversable_agent.py,sha256=-BNwxfqSaHQi428a6xgz_53ThPC8LDsdHzHiWzNsPtc,192671
28
+ autogen/agentchat/conversable_agent.py,sha256=v-vX9HkzJzrOXqzLCxXCow2aND0sjcbqR7h4ECKgNAM,193375
29
29
  autogen/agentchat/groupchat.py,sha256=vdK8zUX2CG8fR5pkPGWX7Zn7DFwLvld0bpaRv3vOBEo,89471
30
30
  autogen/agentchat/user_proxy_agent.py,sha256=-gbDblRvE09FGuVB6-y5ZT9Cpvv--rM3FMi8PnPIFBA,7445
31
31
  autogen/agentchat/utils.py,sha256=2ZweUqe4ynxji0jnvd0r9Uxg3n0elif4a-jZOyeeqcg,8238
@@ -212,7 +212,7 @@ autogen/cache/in_memory_cache.py,sha256=qeECs4UnN5FzuCSgiF-Ma11eLGSjJBgGlqLraWhD
212
212
  autogen/cache/redis_cache.py,sha256=XqUVObZRbD_kDL5vDD6H6xu_U-xcL8qKmsUq2Ix8xVo,4248
213
213
  autogen/coding/__init__.py,sha256=fN8UCm3RzJZ4OFMwlp3a0jic0ZzOege6TNVAk5t6J0U,825
214
214
  autogen/coding/base.py,sha256=hj70EwB-b0MVzHXACw-Oj3CiHo-GUC5kGZGniDTMOnw,3801
215
- autogen/coding/docker_commandline_code_executor.py,sha256=njZM__TUdQyIHdIx2tH4DBtptFzjIrQwyxpt2CJgMGo,9900
215
+ autogen/coding/docker_commandline_code_executor.py,sha256=8sr6bBpk1JtWEb1YQNgmj5z_n2mL_-cAP3EzSrn6SLU,10872
216
216
  autogen/coding/factory.py,sha256=bC3kkIBNExAPt8KZvCQ1PsLpyJWLq1jpw1JcvIbKR04,1978
217
217
  autogen/coding/func_with_reqs.py,sha256=dPcjRs4gOjoVO3m5cQYWtFm7FKO94gBYPwr3O7gVj7g,6372
218
218
  autogen/coding/local_commandline_code_executor.py,sha256=XKkT141IYZf_HngNJjMcJ7uZXe3k056wMdXZC2hfAts,16671
@@ -226,6 +226,12 @@ autogen/coding/jupyter/import_utils.py,sha256=7yzEAYmNmJsODsqz3XCNOORKfvp0t6xnaf
226
226
  autogen/coding/jupyter/jupyter_client.py,sha256=ROXAWOKG_EJ_oFNuyqUd_3uOBPUTRoTh6_-8bSpXBdU,8674
227
227
  autogen/coding/jupyter/jupyter_code_executor.py,sha256=Z2vZvou6QzpMBg0IgOzVRoCADswd15mvfkktIjGhUMY,6374
228
228
  autogen/coding/jupyter/local_jupyter_server.py,sha256=7b8yi5qK8ms2e5-PRCrzmXKGp1iC5KgpMU8xiqQ9u8o,6589
229
+ autogen/environments/__init__.py,sha256=SNh4NFcBgySNW--H1YUZPEf7F7QLpBQA3Qc8EvyM-gk,488
230
+ autogen/environments/docker_python_environment.py,sha256=oKdtr2IN2UDoYi8V_Qjvut5VGfwUNzNoj8J1aMRP9GU,15312
231
+ autogen/environments/python_environment.py,sha256=CXY0h3SWJPYSZtcR_nqY5VmVvGItIJwAnBqSx7YvECg,4265
232
+ autogen/environments/system_python_environment.py,sha256=jk1U4CSDPsF1igqdv-GQOKsH_NTxitRzN35-UWng2e8,3081
233
+ autogen/environments/venv_python_environment.py,sha256=i7dgd8lFuxfZwkysnFXuOn_B26CFYCzNHHBfCtExLkk,10076
234
+ autogen/environments/working_directory.py,sha256=MevUpN1lzmJLEKM_o9JHZod9cy9pQgI5AsE0ic6fvrE,2505
229
235
  autogen/events/__init__.py,sha256=XwCA6Rsq9AyjgeecGuiwHcAvDQMmKXgGomw5iLDIU5Q,358
230
236
  autogen/events/agent_events.py,sha256=LSCOMwA-nlBSboE5jOh-Da2Pm7hghiwjfzYTz_bS30k,31112
231
237
  autogen/events/base_event.py,sha256=5K1wzDBAio9wLxahErSvE0-UbFfMuSTxBp6EI8SbPGU,3475
@@ -274,14 +280,14 @@ autogen/logger/base_logger.py,sha256=nMR6IqELN8b-TzqL0-pJyJ6CpIejjp45IYh5tsMQHAE
274
280
  autogen/logger/file_logger.py,sha256=EbjDhvPUSP2JX-xAZRplc8JRm6au8ofcjHz-KfXboh4,9796
275
281
  autogen/logger/logger_factory.py,sha256=CeLbW3gN0J5zgvQSsRLCSnaiMYusrDWLWovo_Bk-mK8,1391
276
282
  autogen/logger/logger_utils.py,sha256=H9hcsRyEcUcfxTYWf5cRjtNghF4h3FT8sr4IIuqQumY,2053
277
- autogen/logger/sqlite_logger.py,sha256=sRwMx42zh85QWLz1BqKyVySI8OwEB_NjM3ObLOW-mcI,18685
283
+ autogen/logger/sqlite_logger.py,sha256=rP1He7CJB839Qhn1AvW5C0kQ1c45KJs1lgV16y4LxpQ,18694
278
284
  autogen/mcp/__init__.py,sha256=6BDDmw0sjLZRjyHnd-Gfh9BE-pTKTv5bkow9W6odHtQ,213
279
285
  autogen/mcp/__main__.py,sha256=C7nXbWxG3yGsWKRgFnhSsbQOmshLnz6ZOcCxK2TWWio,2487
280
286
  autogen/mcp/helpers.py,sha256=J5_J6n3jMJUEJH5K8k9BeUb6ymQgRUI0hC1gbY7rlwM,1533
281
287
  autogen/mcp/mcp_client.py,sha256=7c_lHgBJEs77TFYjLcTlVrEu_0z4EafPPY3PgteY87c,7400
282
288
  autogen/mcp/mcp_proxy/__init__.py,sha256=3HTU-TqHLk4XSXeBV1UFd9XkQ1B0yOuXXyGseXvDVec,518
283
289
  autogen/mcp/mcp_proxy/fastapi_code_generator_helpers.py,sha256=dx-w2tGVMnh8pzY2NuXlMD7oIF7_5Gvc5oSbHIySEpc,2110
284
- autogen/mcp/mcp_proxy/mcp_proxy.py,sha256=NfjNsUnqpPQ_FrKJPiPUL7aDE9SZ_wNB0ZHAsiJHq9I,22131
290
+ autogen/mcp/mcp_proxy/mcp_proxy.py,sha256=1eWwZatPgPxqkhcbEFCW-ErmkrWUF9F_cLkFWVtLkug,22171
285
291
  autogen/mcp/mcp_proxy/operation_grouping.py,sha256=1n4o5qhkQd2hVr7OaOMsRhInDveDGkCozLudsBVusC4,6349
286
292
  autogen/mcp/mcp_proxy/operation_renaming.py,sha256=G5J4VdxUAwSvOo-DsqIUbCfV9TnH3riaHLI6IpctDF8,3956
287
293
  autogen/mcp/mcp_proxy/patch_fastapi_code_generator.py,sha256=vBr8P890nsFvK4iuFicrdNskooHlBQeTQ6jbb5kcPAk,3490
@@ -296,16 +302,16 @@ autogen/oai/__init__.py,sha256=BIwnV6wtHmKgIM4IUdymfPfpdNqos5P7BfRv-7_QL9A,1680
296
302
  autogen/oai/anthropic.py,sha256=AK_Bbcc5I-PqAvoiwB0LaO1ZLugh_wQNAOIFcj7eTtk,29174
297
303
  autogen/oai/bedrock.py,sha256=8AYWZVsDkJS2HmQ0ggoUqlKV_a4H_ON8rks4VW2Jrxk,25719
298
304
  autogen/oai/cerebras.py,sha256=8hiSBq88l2yTXUJPV7AvGXRCtwvW0Y9hIYUnYK2S2os,12462
299
- autogen/oai/client.py,sha256=nXPRAiUrO5MoGGK2CjKG8TNKfuJxRCDdGVHKQGKk0H4,66957
305
+ autogen/oai/client.py,sha256=icTgPuHiO6Q9l3iu34j2JYXLCSzYVuculfMBEm1TeDY,68897
300
306
  autogen/oai/client_utils.py,sha256=lVbHyff7OnpdM-tXskC23xLdFccj2AalTdWA4DxzxS4,7543
301
307
  autogen/oai/cohere.py,sha256=pRcQWjbzKbZ1RfC1vk9WGjgndwjHbIaOVoKEYdV2L6c,19421
302
- autogen/oai/gemini.py,sha256=bc_RQwtoGi7DnwQwPie7ZdvsqpD1AjYwzjVwnIKP39U,43168
308
+ autogen/oai/gemini.py,sha256=PXHLyXJgB35YuRFGnM_8fxg2FDRcQj3szh0PZc9cOKA,43192
303
309
  autogen/oai/gemini_types.py,sha256=i4wT8ytD2cO9nCUWm2FKLKVVxyd31wMhOpnrvnIiKIc,5888
304
310
  autogen/oai/groq.py,sha256=pQWtaAY_AjT30XKbZNHXDzWsawBys3yFWlfy6K4Nqr8,12431
305
311
  autogen/oai/mistral.py,sha256=SlOYPdnNLHuTEHBGCzsemG9sLEgphdUukRurERdMsvI,12677
306
- autogen/oai/ollama.py,sha256=t0fIgDCoIfsQZ3hhpseam5N-fbpI7-fw82bG55mA8nU,29103
312
+ autogen/oai/ollama.py,sha256=39r_dA9Uq_ZcVxxMmaSJ9WgjxspQty7PX6cdx8lOEpQ,29162
307
313
  autogen/oai/openai_responses.py,sha256=W847OTGzlDbII8sYWIXTOc9NEPAM59XmLWvcYbC6bRo,17811
308
- autogen/oai/openai_utils.py,sha256=4kEu50WeTGGG2uh1fOeMxRIZkEoov-YkkTgx2n5DhkM,38131
314
+ autogen/oai/openai_utils.py,sha256=lVWSJsTT_Kpto00423mf0zkDLCVXbiYLxLeiG9QdjQ4,41401
309
315
  autogen/oai/together.py,sha256=Sj4LOk9RrBG3Bb5IVsrjBYz-hDECCyCgofsCdtk6PSM,14867
310
316
  autogen/oai/oai_models/__init__.py,sha256=cILDaaCCvSC3aAX85iLwE1RCpNEokA9925Zse5hX2K4,549
311
317
  autogen/oai/oai_models/_models.py,sha256=jr5nlvk7Be4W7wDVnwyjDL6m2CSj0RY1nOL1W3Kq0xI,478
@@ -318,14 +324,16 @@ autogen/oai/oai_models/completion_usage.py,sha256=WAuvTPlONP3wN1iiWXztp7Zv6e6-ba
318
324
  autogen/tools/__init__.py,sha256=guevGJut7uN2pGpPn9mODaVsTSfeHrGpjjjIEWRBqVo,554
319
325
  autogen/tools/dependency_injection.py,sha256=UaLO9QdaVxPLjbKa5ZRuizPagMfg91pa7ohc54mUkDU,8468
320
326
  autogen/tools/function_utils.py,sha256=J-3u-Ao_kK7PxqiJMea2P-HGfSTeJTXlWTFSI19IndM,13756
321
- autogen/tools/tool.py,sha256=1VM5wkPaWrsndCEEYAuFODby0du3raRWCBAGO-kR2xo,7243
327
+ autogen/tools/tool.py,sha256=VtSYf363Z9vIsSZWgNnYs9REdRuPWJMDZkcklIq9Yh8,7225
322
328
  autogen/tools/toolkit.py,sha256=1tOmTGJ96RhkJrrtAViKUyEcwacA6ztoIbYbnf8NgTU,2558
323
329
  autogen/tools/contrib/__init__.py,sha256=DWEjPK6xCR2ihAXXdquQZmiuqRLA3Pqb8QV8W1RtS3k,202
324
330
  autogen/tools/contrib/time/__init__.py,sha256=dplie5aBJZ8VoKy6EKcQMLTtSgcCkNDYzpdsC2I0YWk,195
325
331
  autogen/tools/contrib/time/time.py,sha256=tPi49vOUwfvujbYA-zS00CWcLW-y18CPyQ1gnJG6iRg,1271
326
- autogen/tools/experimental/__init__.py,sha256=jGyt9GVoJO6VsZdmBLzwTxXZGBbWEwdLrUjn-rWQ6os,1588
332
+ autogen/tools/experimental/__init__.py,sha256=OPJlrK_tJCPbeBT-qsIWwJBdLuaCMskpshduA8Haj7E,1671
327
333
  autogen/tools/experimental/browser_use/__init__.py,sha256=kfxCajXcVMDH6CZq-lWh2p8PKxOwT9yjC_Za0jr4zUg,290
328
334
  autogen/tools/experimental/browser_use/browser_use.py,sha256=KfU4MI_BWaHepv0bDMf9HTDUaHTJThuBJI8R_BPpjmg,5561
335
+ autogen/tools/experimental/code_execution/__init__.py,sha256=KFUku2awEgsnkSa48t2iMMPC-QCxpqAG7lb8fB4Fr-c,242
336
+ autogen/tools/experimental/code_execution/python_code_execution.py,sha256=Fj1Wxsn6z0HuXWpqa-ahedkCtyPYBShUnbiyvSw_mus,3705
329
337
  autogen/tools/experimental/crawl4ai/__init__.py,sha256=UjFJLSZ9P5xT6WCV0RDPtwt4MHuwPdK90TU7ByXhLWs,207
330
338
  autogen/tools/experimental/crawl4ai/crawl4ai.py,sha256=MsOLtbPHRpRrCnRJPQVVVNwmsBcgsWLSHNXDOF-47ws,6088
331
339
  autogen/tools/experimental/deep_research/__init__.py,sha256=9SFcDEj2OHxNSlXP11lf1uHENlfUeO47ROcOSD9GCDs,220
@@ -406,8 +414,8 @@ autogen/agentchat/contrib/captainagent/tools/math/modular_inverse_sum.py,sha256=
406
414
  autogen/agentchat/contrib/captainagent/tools/math/simplify_mixed_numbers.py,sha256=iqgpFJdyBHPPNCqkehSIbeuV8Rabr2eDMilT23Wx7PI,1687
407
415
  autogen/agentchat/contrib/captainagent/tools/math/sum_of_digit_factorials.py,sha256=-6T5r6Er4mONPldRxv3F9tLoE7Og3qmeSeTC7Du_tTg,596
408
416
  autogen/agentchat/contrib/captainagent/tools/math/sum_of_primes_below.py,sha256=Xig7K3A3DRnbv-UXfyo5bybGZUQYAQsltthfTYW5eV8,509
409
- ag2-0.9.5.dist-info/METADATA,sha256=K4Gz-eYvrw_xTPz7UV0WPlKiR5LluSMxvV0PGJZUWpA,35268
410
- ag2-0.9.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
411
- ag2-0.9.5.dist-info/licenses/LICENSE,sha256=GEFQVNayAR-S_rQD5l8hPdgvgyktVdy4Bx5-v90IfRI,11384
412
- ag2-0.9.5.dist-info/licenses/NOTICE.md,sha256=07iCPQGbth4pQrgkSgZinJGT5nXddkZ6_MGYcBd2oiY,1134
413
- ag2-0.9.5.dist-info/RECORD,,
417
+ ag2-0.9.7.dist-info/METADATA,sha256=eFWnorPlwQwD0pmvgJvzwflsiWFZh69HHZq4MgxNxjg,35268
418
+ ag2-0.9.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
419
+ ag2-0.9.7.dist-info/licenses/LICENSE,sha256=GEFQVNayAR-S_rQD5l8hPdgvgyktVdy4Bx5-v90IfRI,11384
420
+ ag2-0.9.7.dist-info/licenses/NOTICE.md,sha256=07iCPQGbth4pQrgkSgZinJGT5nXddkZ6_MGYcBd2oiY,1134
421
+ ag2-0.9.7.dist-info/RECORD,,
@@ -3873,15 +3873,29 @@ class ConversableAgent(LLMAgent):
3873
3873
  **executor_kwargs,
3874
3874
  )
3875
3875
 
3876
- tools = [] if tools is None else tools
3877
- tools = [tools] if isinstance(tools, Tool) else tools
3878
- for tool in tools:
3876
+ # Combine agent's existing tools with passed tools
3877
+ agent_tools = self._tools.copy() # Get agent's pre-registered tools
3878
+ passed_tools = [] if tools is None else tools
3879
+ passed_tools = [passed_tools] if isinstance(passed_tools, Tool) else passed_tools
3880
+
3881
+ # Combine both sets of tools (avoid duplicates)
3882
+ all_tools = agent_tools.copy()
3883
+ for tool in passed_tools:
3884
+ if tool not in all_tools:
3885
+ all_tools.append(tool)
3886
+
3887
+ # Register all tools with the executor
3888
+ for tool in all_tools:
3879
3889
  tool.register_for_execution(self.run_executor)
3890
+
3891
+ # Register only newly passed tools for LLM (agent's pre-existing tools are already registered)
3892
+ for tool in passed_tools:
3880
3893
  tool.register_for_llm(self)
3881
3894
  yield self.run_executor
3882
3895
  finally:
3883
- if tools is not None:
3884
- for tool in tools:
3896
+ # Clean up only newly passed tools (not agent's pre-existing tools)
3897
+ if "passed_tools" in locals():
3898
+ for tool in passed_tools:
3885
3899
  self.update_tool_signature(tool_sig=tool.tool_schema["function"]["name"], is_remove=True)
3886
3900
 
3887
3901
  def _deprecated_run(
@@ -71,6 +71,8 @@ class DockerCommandLineCodeExecutor(CodeExecutor):
71
71
  auto_remove: bool = True,
72
72
  stop_container: bool = True,
73
73
  execution_policies: Optional[dict[str, bool]] = None,
74
+ *,
75
+ container_create_kwargs: Optional[dict[str, Any]] = None,
74
76
  ):
75
77
  """(Experimental) A code executor class that executes code through
76
78
  a command line environment in a Docker container.
@@ -98,6 +100,11 @@ class DockerCommandLineCodeExecutor(CodeExecutor):
98
100
  whether code in that language should be executed. True means code in that language
99
101
  will be executed, False means it will only be saved to a file. This overrides the
100
102
  default execution policies. Defaults to None.
103
+ container_create_kwargs: Optional dict forwarded verbatim to
104
+ "docker.client.containers.create". Use it to set advanced Docker
105
+ options (environment variables, GPU device_requests, port mappings, etc.).
106
+ Values here override the class defaults when keys collide. Defaults to None.
107
+
101
108
 
102
109
  Raises:
103
110
  ValueError: On argument error, or if the container fails to start.
@@ -128,16 +135,29 @@ class DockerCommandLineCodeExecutor(CodeExecutor):
128
135
  if container_name is None:
129
136
  container_name = f"autogen-code-exec-{uuid.uuid4()}"
130
137
 
138
+ # build kwargs for docker.create
139
+ base_kwargs: dict[str, Any] = {
140
+ "image": image,
141
+ "name": container_name,
142
+ "entrypoint": "/bin/sh",
143
+ "tty": True,
144
+ "auto_remove": auto_remove,
145
+ "volumes": {str(bind_dir.resolve()): {"bind": "/workspace", "mode": "rw"}},
146
+ "working_dir": "/workspace",
147
+ }
148
+
149
+ if container_create_kwargs:
150
+ for k in ("entrypoint", "volumes", "working_dir", "tty"):
151
+ if k in container_create_kwargs:
152
+ logging.warning(
153
+ "DockerCommandLineCodeExecutor: overriding default %s=%s",
154
+ k,
155
+ container_create_kwargs[k],
156
+ )
157
+ base_kwargs.update(container_create_kwargs)
158
+
131
159
  # Start a container from the image, read to exec commands later
132
- self._container = client.containers.create(
133
- image,
134
- name=container_name,
135
- entrypoint="/bin/sh",
136
- tty=True,
137
- auto_remove=auto_remove,
138
- volumes={str(bind_dir.resolve()): {"bind": "/workspace", "mode": "rw"}},
139
- working_dir="/workspace",
140
- )
160
+ self._container = client.containers.create(**base_kwargs)
141
161
  self._container.start()
142
162
 
143
163
  _wait_for_ready(self._container)
@@ -0,0 +1,10 @@
1
+ # Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from .docker_python_environment import DockerPythonEnvironment
6
+ from .system_python_environment import SystemPythonEnvironment
7
+ from .venv_python_environment import VenvPythonEnvironment
8
+ from .working_directory import WorkingDirectory
9
+
10
+ __all__ = ["DockerPythonEnvironment", "SystemPythonEnvironment", "VenvPythonEnvironment", "WorkingDirectory"]
@@ -0,0 +1,375 @@
1
+ # Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import logging
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ import uuid
11
+ from typing import Any, Optional, Tuple
12
+
13
+ from asyncer import asyncify
14
+
15
+ from .python_environment import PythonEnvironment
16
+
17
+ __all__ = ["DockerPythonEnvironment"]
18
+
19
+
20
+ class DockerPythonEnvironment(PythonEnvironment):
21
+ """A Python environment using Docker containers for isolated execution."""
22
+
23
+ def __init__(
24
+ self,
25
+ image: str = "python:3.11-slim",
26
+ container_name_prefix: str = "ag2_docker_env_",
27
+ volumes: Optional[dict[str, str]] = None,
28
+ environment: Optional[dict[str, str]] = None,
29
+ network: Optional[str] = None,
30
+ pip_packages: Optional[list[str]] = None,
31
+ requirements_file: Optional[str] = None,
32
+ dockerfile: Optional[str] = None,
33
+ build_args: Optional[dict[str, str]] = None,
34
+ cleanup_container: bool = True,
35
+ keep_container_running: bool = False,
36
+ container_startup_timeout: int = 30,
37
+ ):
38
+ """
39
+ Initialize a Docker Python environment.
40
+
41
+ Args:
42
+ image: Docker image to use (ignored if dockerfile is provided)
43
+ container_name_prefix: Prefix for container names
44
+ volumes: Dictionary mapping host paths to container paths for mounting
45
+ environment: Dictionary of environment variables to set in the container
46
+ network: Docker network to attach the container to
47
+ pip_packages: List of pip packages to install in the container
48
+ requirements_file: Path to requirements.txt file to install in the container
49
+ dockerfile: Optional path to a Dockerfile to build and use instead of pulling an image
50
+ build_args: Optional build arguments for the Dockerfile
51
+ cleanup_container: Whether to remove the container after use
52
+ keep_container_running: Whether to keep the container running after execution
53
+ container_startup_timeout: Timeout in seconds for container startup
54
+ """
55
+ self.image = image
56
+ self.container_name_prefix = container_name_prefix
57
+ self.volumes = volumes or {}
58
+ self.environment = environment or {}
59
+ self.network = network
60
+ self.pip_packages = pip_packages or []
61
+ self.requirements_file = requirements_file
62
+ self.dockerfile = dockerfile
63
+ self.build_args = build_args or {}
64
+ self.cleanup_container = cleanup_container
65
+ self.keep_container_running = keep_container_running
66
+ self.container_startup_timeout = container_startup_timeout
67
+
68
+ # Internal state
69
+ self._container_id = None
70
+ self._container_name = None
71
+ self._custom_image_name = None
72
+ self._temp_dir = None
73
+
74
+ super().__init__()
75
+
76
+ def _setup_environment(self) -> None:
77
+ """Set up the Docker environment."""
78
+ # Verify Docker is installed and accessible
79
+ try:
80
+ result = subprocess.run(["docker", "--version"], capture_output=True, text=True, check=True)
81
+ logging.info(f"Docker version: {result.stdout.strip()}")
82
+ except (subprocess.SubprocessError, FileNotFoundError) as e:
83
+ raise RuntimeError(
84
+ "Docker not found or not accessible. Please ensure Docker is installed and running."
85
+ ) from e
86
+
87
+ # Create a temporary directory for file operations
88
+ self._temp_dir = tempfile.mkdtemp(prefix="ag2_docker_")
89
+
90
+ # Generate a unique container name
91
+ self._container_name = f"{self.container_name_prefix}{uuid.uuid4().hex[:8]}"
92
+
93
+ # Build custom image if Dockerfile is provided
94
+ if self.dockerfile:
95
+ self._build_custom_image()
96
+ else:
97
+ # Pull the specified image
98
+ try:
99
+ subprocess.run(
100
+ ["docker", "pull", self.image],
101
+ check=True,
102
+ stdout=subprocess.PIPE,
103
+ stderr=subprocess.PIPE,
104
+ text=True,
105
+ )
106
+ logging.info(f"Pulled Docker image: {self.image}")
107
+ except subprocess.CalledProcessError as e:
108
+ raise RuntimeError(f"Failed to pull Docker image: {e.stderr}") from e
109
+
110
+ # Start the container
111
+ self._start_container()
112
+
113
+ def _build_custom_image(self) -> None:
114
+ """Build a custom Docker image from the provided Dockerfile."""
115
+ if not os.path.exists(self.dockerfile):
116
+ raise RuntimeError(f"Dockerfile not found at: {self.dockerfile}")
117
+
118
+ # Create a unique image name
119
+ self._custom_image_name = f"ag2-custom-python-{uuid.uuid4().hex[:8]}"
120
+
121
+ # Build command
122
+ build_cmd = ["docker", "build", "-t", self._custom_image_name]
123
+
124
+ # Add build args
125
+ for arg_name, arg_value in self.build_args.items():
126
+ build_cmd.extend(["--build-arg", f"{arg_name}={arg_value}"])
127
+
128
+ # Add Dockerfile path
129
+ build_cmd.extend(["-f", self.dockerfile, os.path.dirname(self.dockerfile)])
130
+
131
+ try:
132
+ logging.info(f"Building custom Docker image: {self._custom_image_name}")
133
+ _ = subprocess.run(
134
+ build_cmd,
135
+ check=True,
136
+ stdout=subprocess.PIPE,
137
+ stderr=subprocess.PIPE,
138
+ text=True,
139
+ )
140
+ logging.info(f"Built custom Docker image: {self._custom_image_name}")
141
+ except subprocess.CalledProcessError as e:
142
+ raise RuntimeError(f"Failed to build Docker image: {e.stderr}") from e
143
+
144
+ # Use the custom image
145
+ self.image = self._custom_image_name
146
+
147
+ def _start_container(self) -> None:
148
+ """Start the Docker container."""
149
+ # Basic container run command
150
+ run_cmd = ["docker", "run", "--name", self._container_name]
151
+
152
+ # Add detached mode flag to run container in background
153
+ run_cmd.append("-d")
154
+
155
+ # Add network if specified
156
+ if self.network:
157
+ run_cmd.extend(["--network", self.network])
158
+
159
+ # Add environment variables
160
+ for env_name, env_value in self.environment.items():
161
+ run_cmd.extend(["-e", f"{env_name}={env_value}"])
162
+
163
+ # Add volume mounts including temp directory
164
+ work_dir_mount = f"{self._temp_dir}:/workspace"
165
+ run_cmd.extend(["-v", work_dir_mount])
166
+
167
+ for host_path, container_path in self.volumes.items():
168
+ run_cmd.extend(["-v", f"{host_path}:{container_path}"])
169
+
170
+ # Set workspace as working directory
171
+ run_cmd.extend(["-w", "/workspace"])
172
+
173
+ # Add tty to keep container running
174
+ run_cmd.append("-t")
175
+
176
+ # Add image name
177
+ run_cmd.append(self.image)
178
+
179
+ # Initial command to keep container running
180
+ run_cmd.extend(["tail", "-f", "/dev/null"])
181
+
182
+ try:
183
+ # Start the container
184
+ logging.info(f"Starting Docker container: {self._container_name}")
185
+ result = subprocess.run(
186
+ run_cmd,
187
+ check=True,
188
+ stdout=subprocess.PIPE,
189
+ stderr=subprocess.PIPE,
190
+ text=True,
191
+ )
192
+
193
+ # Get container ID
194
+ self._container_id = result.stdout.strip()
195
+ logging.info(f"Started Docker container: {self._container_name} ({self._container_id})")
196
+
197
+ # Install pip packages if specified
198
+ if self.pip_packages or self.requirements_file:
199
+ self._install_packages()
200
+
201
+ except subprocess.CalledProcessError as e:
202
+ raise RuntimeError(f"Failed to start Docker container: {e.stderr}") from e
203
+
204
+ def _install_packages(self) -> None:
205
+ """Install Python packages in the running container."""
206
+ # Install pip packages
207
+ if self.pip_packages:
208
+ packages_str = " ".join(self.pip_packages)
209
+ try:
210
+ logging.info(f"Installing pip packages: {packages_str}")
211
+ _ = subprocess.run(
212
+ ["docker", "exec", self._container_name, "pip", "install", "--no-cache-dir"] + self.pip_packages,
213
+ check=True,
214
+ stdout=subprocess.PIPE,
215
+ stderr=subprocess.PIPE,
216
+ text=True,
217
+ )
218
+ logging.info("Successfully installed pip packages")
219
+ except subprocess.CalledProcessError as e:
220
+ logging.warning(f"Failed to install pip packages: {e.stderr}")
221
+
222
+ # Install from requirements file
223
+ if self.requirements_file:
224
+ if os.path.exists(self.requirements_file):
225
+ # Copy requirements file to temp directory
226
+ req_filename = os.path.basename(self.requirements_file)
227
+ temp_req_path = os.path.join(self._temp_dir, req_filename)
228
+ shutil.copy(self.requirements_file, temp_req_path)
229
+
230
+ try:
231
+ logging.info(f"Installing requirements from: {req_filename}")
232
+ _ = subprocess.run(
233
+ [
234
+ "docker",
235
+ "exec",
236
+ self._container_name,
237
+ "pip",
238
+ "install",
239
+ "--no-cache-dir",
240
+ "-r",
241
+ f"/workspace/{req_filename}",
242
+ ],
243
+ check=True,
244
+ stdout=subprocess.PIPE,
245
+ stderr=subprocess.PIPE,
246
+ text=True,
247
+ )
248
+ logging.info("Successfully installed requirements")
249
+ except subprocess.CalledProcessError as e:
250
+ logging.warning(f"Failed to install requirements: {e.stderr}")
251
+ else:
252
+ logging.warning(f"Requirements file not found: {self.requirements_file}")
253
+
254
+ def _cleanup_environment(self) -> None:
255
+ """Clean up the Docker environment."""
256
+ if self._container_id:
257
+ # Stop the container if it's running and we want to clean it up
258
+ if not self.keep_container_running:
259
+ try:
260
+ logging.info(f"Stopping Docker container: {self._container_name}")
261
+ subprocess.run(
262
+ ["docker", "stop", self._container_name],
263
+ check=True,
264
+ stdout=subprocess.PIPE,
265
+ stderr=subprocess.PIPE,
266
+ text=True,
267
+ )
268
+ except subprocess.CalledProcessError:
269
+ logging.warning(f"Failed to stop Docker container: {self._container_name}")
270
+
271
+ # Remove the container if cleanup is enabled
272
+ if self.cleanup_container and not self.keep_container_running:
273
+ try:
274
+ logging.info(f"Removing Docker container: {self._container_name}")
275
+ subprocess.run(
276
+ ["docker", "rm", "-f", self._container_name],
277
+ check=True,
278
+ stdout=subprocess.PIPE,
279
+ stderr=subprocess.PIPE,
280
+ text=True,
281
+ )
282
+ except subprocess.CalledProcessError:
283
+ logging.warning(f"Failed to remove Docker container: {self._container_name}")
284
+
285
+ # Remove the custom image if it was created
286
+ if self._custom_image_name and self.cleanup_container:
287
+ try:
288
+ logging.info(f"Removing custom Docker image: {self._custom_image_name}")
289
+ subprocess.run(
290
+ ["docker", "rmi", self._custom_image_name],
291
+ check=True,
292
+ stdout=subprocess.PIPE,
293
+ stderr=subprocess.PIPE,
294
+ text=True,
295
+ )
296
+ except subprocess.CalledProcessError:
297
+ logging.warning(f"Failed to remove custom Docker image: {self._custom_image_name}")
298
+
299
+ # Clean up the temporary directory
300
+ if self._temp_dir and os.path.exists(self._temp_dir):
301
+ try:
302
+ shutil.rmtree(self._temp_dir)
303
+ except Exception as e:
304
+ logging.warning(f"Failed to remove temporary directory: {e}")
305
+
306
+ def get_executable(self) -> str:
307
+ """Get the path to the Python executable in the Docker container."""
308
+ # This is a virtual path in the container
309
+ return "python"
310
+
311
+ async def execute_code(self, code: str, script_path: str, timeout: int = 30) -> dict[str, Any]:
312
+ """Execute code in the Docker container."""
313
+ # Ensure the container is running
314
+ if not self._container_id:
315
+ return {"success": False, "error": "Docker container not started"}
316
+
317
+ try:
318
+ # Calculate the relative path within the temp directory
319
+ if os.path.isabs(script_path):
320
+ rel_path = os.path.basename(script_path)
321
+ host_script_path = os.path.join(self._temp_dir, rel_path)
322
+ else:
323
+ rel_path = script_path
324
+ host_script_path = os.path.join(self._temp_dir, rel_path)
325
+
326
+ # Ensure the directory for the script exists
327
+ script_dir = os.path.dirname(host_script_path)
328
+ if script_dir:
329
+ os.makedirs(script_dir, exist_ok=True)
330
+
331
+ # Write the code to the script file on the host
332
+ await asyncify(self._write_to_file)(host_script_path, code)
333
+
334
+ # Path to the script in the container
335
+ container_script_path = f"/workspace/{rel_path}"
336
+
337
+ # Execute the script in the container
338
+ exec_cmd = ["docker", "exec", self._container_name, "python", container_script_path]
339
+
340
+ # Run the command with a timeout
341
+ result = await asyncify(self._run_subprocess_with_timeout)(exec_cmd, timeout)
342
+
343
+ return {
344
+ "success": result[0],
345
+ "stdout": result[1],
346
+ "stderr": result[2],
347
+ "returncode": result[3] if result[0] else 1,
348
+ }
349
+
350
+ except Exception as e:
351
+ return {"success": False, "error": f"Execution error: {str(e)}"}
352
+
353
+ def _run_subprocess_with_timeout(self, cmd: list[str], timeout: int) -> Tuple[bool, str, str, int]:
354
+ """
355
+ Run a subprocess with timeout and return status, stdout, stderr, and return code.
356
+
357
+ Args:
358
+ cmd: Command to run as a list of strings
359
+ timeout: Maximum execution time in seconds
360
+
361
+ Returns:
362
+ Tuple of (success, stdout, stderr, return_code)
363
+ """
364
+ try:
365
+ result = subprocess.run(
366
+ cmd,
367
+ capture_output=True,
368
+ text=True,
369
+ timeout=timeout,
370
+ )
371
+ return (result.returncode == 0, result.stdout, result.stderr, result.returncode)
372
+ except subprocess.TimeoutExpired:
373
+ return (False, "", f"Execution timed out after {timeout} seconds", -1)
374
+ except Exception as e:
375
+ return (False, "", str(e), -1)