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.
- {ag2-0.9.5.dist-info → ag2-0.9.7.dist-info}/METADATA +1 -1
- {ag2-0.9.5.dist-info → ag2-0.9.7.dist-info}/RECORD +25 -17
- autogen/agentchat/conversable_agent.py +19 -5
- autogen/coding/docker_commandline_code_executor.py +29 -9
- autogen/environments/__init__.py +10 -0
- autogen/environments/docker_python_environment.py +375 -0
- autogen/environments/python_environment.py +134 -0
- autogen/environments/system_python_environment.py +86 -0
- autogen/environments/venv_python_environment.py +224 -0
- autogen/environments/working_directory.py +75 -0
- autogen/llm_config.py +6 -3
- autogen/logger/sqlite_logger.py +1 -1
- autogen/mcp/mcp_proxy/mcp_proxy.py +4 -4
- autogen/oai/client.py +34 -7
- autogen/oai/gemini.py +1 -0
- autogen/oai/ollama.py +3 -1
- autogen/oai/openai_utils.py +111 -47
- autogen/tools/experimental/__init__.py +2 -0
- autogen/tools/experimental/code_execution/__init__.py +7 -0
- autogen/tools/experimental/code_execution/python_code_execution.py +88 -0
- autogen/tools/tool.py +1 -2
- autogen/version.py +1 -1
- {ag2-0.9.5.dist-info → ag2-0.9.7.dist-info}/WHEEL +0 -0
- {ag2-0.9.5.dist-info → ag2-0.9.7.dist-info}/licenses/LICENSE +0 -0
- {ag2-0.9.5.dist-info → ag2-0.9.7.dist-info}/licenses/NOTICE.md +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
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 subprocess
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from contextvars import ContextVar
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
__all__ = ["PythonEnvironment"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PythonEnvironment(ABC):
|
|
14
|
+
"""Python execution environments base class"""
|
|
15
|
+
|
|
16
|
+
# Shared context variable for tracking the current environment
|
|
17
|
+
_current_python_environment: ContextVar["PythonEnvironment"] = ContextVar("_current_python_environment")
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""
|
|
21
|
+
Initialize the Python environment.
|
|
22
|
+
"""
|
|
23
|
+
self._token = None
|
|
24
|
+
|
|
25
|
+
# Set up the environment
|
|
26
|
+
self._setup_environment()
|
|
27
|
+
|
|
28
|
+
def __enter__(self):
|
|
29
|
+
"""
|
|
30
|
+
Enter the environment context.
|
|
31
|
+
Sets this environment as the current one.
|
|
32
|
+
"""
|
|
33
|
+
# Set this as the current Python environment in the context
|
|
34
|
+
self._token = PythonEnvironment._current_python_environment.set(self)
|
|
35
|
+
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
39
|
+
"""
|
|
40
|
+
Exit the environment context.
|
|
41
|
+
Resets the current environment and performs cleanup.
|
|
42
|
+
"""
|
|
43
|
+
# Reset the context variable if this was the active environment
|
|
44
|
+
if self._token is not None:
|
|
45
|
+
PythonEnvironment._current_python_environment.reset(self._token)
|
|
46
|
+
self._token = None
|
|
47
|
+
|
|
48
|
+
# Clean up resources
|
|
49
|
+
self._cleanup_environment()
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def _setup_environment(self) -> None:
|
|
53
|
+
"""Set up the Python environment. Called by __enter__."""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def _cleanup_environment(self) -> None:
|
|
58
|
+
"""Clean up the Python environment. Called by __exit__."""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
def get_executable(self) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Get the path to the Python executable in this environment.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The full path to the Python executable.
|
|
68
|
+
"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
async def execute_code(self, code: str, script_path: str, timeout: int = 30) -> dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Execute the given code in this environment.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
code: The Python code to execute.
|
|
78
|
+
script_path: Path where the code should be saved before execution.
|
|
79
|
+
timeout: Maximum execution time in seconds.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
dict with execution results including stdout, stderr, and success status.
|
|
83
|
+
"""
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Utility method for subclasses to wrap (for async support)
|
|
87
|
+
def _write_to_file(self, script_path: str, content: str) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Write content to a file (blocking operation).
|
|
90
|
+
|
|
91
|
+
This is a helper method for use with asyncify in async contexts.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
script_path: Path to the file to write.
|
|
95
|
+
content: Content to write to the file.
|
|
96
|
+
"""
|
|
97
|
+
with open(script_path, "w") as f:
|
|
98
|
+
f.write(content)
|
|
99
|
+
|
|
100
|
+
# Utility method for subclasses to wrap (for async support)
|
|
101
|
+
def _run_subprocess(self, cmd: list[str], timeout: int) -> subprocess.CompletedProcess:
|
|
102
|
+
"""
|
|
103
|
+
Run a subprocess (blocking operation).
|
|
104
|
+
|
|
105
|
+
This is a helper method for use with asyncify in async contexts.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
cmd: Command to run as a list of strings.
|
|
109
|
+
timeout: Maximum execution time in seconds.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
CompletedProcess instance with results of the subprocess.
|
|
113
|
+
"""
|
|
114
|
+
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def get_current_python_environment(
|
|
118
|
+
cls, python_environment: Optional["PythonEnvironment"] = None
|
|
119
|
+
) -> Optional["PythonEnvironment"]:
|
|
120
|
+
"""
|
|
121
|
+
Get the current Python environment or the specified one if provided.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
python_environment: Optional environment to return if specified.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The current Python environment or None if none is active.
|
|
128
|
+
"""
|
|
129
|
+
if python_environment is not None:
|
|
130
|
+
return python_environment
|
|
131
|
+
try:
|
|
132
|
+
return cls._current_python_environment.get()
|
|
133
|
+
except LookupError:
|
|
134
|
+
return None
|
|
@@ -0,0 +1,86 @@
|
|
|
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 subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from asyncer import asyncify
|
|
12
|
+
|
|
13
|
+
from .python_environment import PythonEnvironment
|
|
14
|
+
|
|
15
|
+
__all__ = ["SystemPythonEnvironment"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SystemPythonEnvironment(PythonEnvironment):
|
|
19
|
+
"""A Python environment using the system's Python installation."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
executable: Optional[str] = None,
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Initialize a system Python environment.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
executable: Optional path to a specific Python executable. If None, uses the current Python executable.
|
|
30
|
+
"""
|
|
31
|
+
self._executable = executable or sys.executable
|
|
32
|
+
super().__init__()
|
|
33
|
+
|
|
34
|
+
def _setup_environment(self) -> None:
|
|
35
|
+
"""Set up the system Python environment."""
|
|
36
|
+
# Verify the Python executable exists
|
|
37
|
+
if not os.path.exists(self._executable):
|
|
38
|
+
raise RuntimeError(f"Python executable not found at: {self._executable}")
|
|
39
|
+
|
|
40
|
+
logging.info(f"Using system Python at: {self._executable}")
|
|
41
|
+
|
|
42
|
+
def _cleanup_environment(self) -> None:
|
|
43
|
+
"""Clean up the system Python environment."""
|
|
44
|
+
# No cleanup needed for system Python
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
def get_executable(self) -> str:
|
|
48
|
+
"""Get the path to the Python executable."""
|
|
49
|
+
return self._executable
|
|
50
|
+
|
|
51
|
+
async def execute_code(self, code: str, script_path: str, timeout: int = 30) -> dict[str, Any]:
|
|
52
|
+
"""Execute code using the system Python."""
|
|
53
|
+
try:
|
|
54
|
+
# Get the Python executable
|
|
55
|
+
python_executable = self.get_executable()
|
|
56
|
+
|
|
57
|
+
# Verify the executable exists
|
|
58
|
+
if not os.path.exists(python_executable):
|
|
59
|
+
return {"success": False, "error": f"Python executable not found at {python_executable}"}
|
|
60
|
+
|
|
61
|
+
# Ensure the directory for the script exists
|
|
62
|
+
script_dir = os.path.dirname(script_path)
|
|
63
|
+
if script_dir:
|
|
64
|
+
os.makedirs(script_dir, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
# Write the code to the script file using asyncify (from base class)
|
|
67
|
+
await asyncify(self._write_to_file)(script_path, code)
|
|
68
|
+
|
|
69
|
+
logging.info(f"Wrote code to {script_path}")
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Execute directly with subprocess using asyncify for better reliability
|
|
73
|
+
result = await asyncify(self._run_subprocess)([python_executable, script_path], timeout)
|
|
74
|
+
|
|
75
|
+
# Main execution result
|
|
76
|
+
return {
|
|
77
|
+
"success": result.returncode == 0,
|
|
78
|
+
"stdout": result.stdout,
|
|
79
|
+
"stderr": result.stderr,
|
|
80
|
+
"returncode": result.returncode,
|
|
81
|
+
}
|
|
82
|
+
except subprocess.TimeoutExpired:
|
|
83
|
+
return {"success": False, "error": f"Execution timed out after {timeout} seconds"}
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
return {"success": False, "error": f"Execution error: {str(e)}"}
|
|
@@ -0,0 +1,224 @@
|
|
|
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 subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from asyncer import asyncify
|
|
13
|
+
|
|
14
|
+
from .python_environment import PythonEnvironment
|
|
15
|
+
|
|
16
|
+
__all__ = ["VenvPythonEnvironment"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class VenvPythonEnvironment(PythonEnvironment):
|
|
20
|
+
"""A Python environment using a virtual environment (venv)."""
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
python_version: Optional[str] = None,
|
|
25
|
+
python_path: Optional[str] = None,
|
|
26
|
+
venv_path: Optional[str] = None,
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Initialize a virtual environment for Python execution.
|
|
30
|
+
|
|
31
|
+
If you pass in a venv_path the path will be checked for a valid venv. If the venv doesn't exist it will be created using the python_version or python_path provided.
|
|
32
|
+
|
|
33
|
+
If the python_version or python_path is provided and the venv_path is not, a temporary directory will be created for venv and it will be setup with the provided python version.
|
|
34
|
+
|
|
35
|
+
If python_path is provided, it will take precedence over python_version.
|
|
36
|
+
|
|
37
|
+
The python version will not be installed if it doesn't exist and a RuntimeError will be raised.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
python_version: The Python version to use (e.g., "3.11"), otherwise defaults to the current executing Python version. Ignored if venv_path is provided and has a valid environment already.
|
|
41
|
+
python_path: Optional direct path to a Python executable to use (must include the executable). Takes precedence over python_version if both are provided.
|
|
42
|
+
venv_path: Optional path for the virtual environment, will create it if it doesn't exist. If None, creates a temp directory.
|
|
43
|
+
"""
|
|
44
|
+
self.python_version = python_version
|
|
45
|
+
self.python_path = python_path
|
|
46
|
+
self.venv_path = venv_path
|
|
47
|
+
self.created_venv = False
|
|
48
|
+
self._executable = None
|
|
49
|
+
super().__init__()
|
|
50
|
+
|
|
51
|
+
def _setup_environment(self) -> None:
|
|
52
|
+
"""Set up the virtual environment."""
|
|
53
|
+
# Create a venv directory if not provided
|
|
54
|
+
if self.venv_path is None:
|
|
55
|
+
self.venv_path = tempfile.mkdtemp(prefix="ag2_python_env_")
|
|
56
|
+
self.created_venv = True
|
|
57
|
+
|
|
58
|
+
# Determine the python version, getting it from the venv if it already has one
|
|
59
|
+
base_python = self._get_python_executable_for_version()
|
|
60
|
+
needs_creation = True
|
|
61
|
+
else:
|
|
62
|
+
# If venv_path is provided, check if it's already a valid venv
|
|
63
|
+
if os.name == "nt": # Windows
|
|
64
|
+
venv_python = os.path.join(self.venv_path, "Scripts", "python.exe")
|
|
65
|
+
else: # Unix-like (Mac/Linux)
|
|
66
|
+
venv_python = os.path.join(self.venv_path, "bin", "python")
|
|
67
|
+
|
|
68
|
+
if os.path.exists(venv_python) and os.access(venv_python, os.X_OK):
|
|
69
|
+
# Valid venv already exists, just use it
|
|
70
|
+
self._executable = venv_python
|
|
71
|
+
logging.info(f"Using existing virtual environment at {self.venv_path}")
|
|
72
|
+
needs_creation = False
|
|
73
|
+
else:
|
|
74
|
+
# Path exists but not a valid venv, or doesn't exist
|
|
75
|
+
if not os.path.exists(self.venv_path):
|
|
76
|
+
os.makedirs(self.venv_path, exist_ok=True)
|
|
77
|
+
self.created_venv = True
|
|
78
|
+
base_python = sys.executable
|
|
79
|
+
needs_creation = True
|
|
80
|
+
|
|
81
|
+
# Only create the venv if needed
|
|
82
|
+
if needs_creation:
|
|
83
|
+
logging.info(f"Creating virtual environment at {self.venv_path} using {base_python}")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Create the virtual environment
|
|
87
|
+
_ = subprocess.run(
|
|
88
|
+
[base_python, "-m", "venv", "--system-site-packages", self.venv_path],
|
|
89
|
+
check=True,
|
|
90
|
+
stdout=subprocess.PIPE,
|
|
91
|
+
stderr=subprocess.PIPE,
|
|
92
|
+
text=True,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Determine the Python executable path
|
|
96
|
+
if os.name == "nt": # Windows
|
|
97
|
+
self._executable = os.path.join(self.venv_path, "Scripts", "python.exe")
|
|
98
|
+
else: # Unix-like (Mac/Linux)
|
|
99
|
+
self._executable = os.path.join(self.venv_path, "bin", "python")
|
|
100
|
+
|
|
101
|
+
# Verify the executable exists
|
|
102
|
+
if not os.path.exists(self._executable):
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
f"Virtual environment created but Python executable not found at {self._executable}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
except subprocess.CalledProcessError as e:
|
|
108
|
+
raise RuntimeError(f"Failed to create virtual environment: {e.stderr}") from e
|
|
109
|
+
|
|
110
|
+
def _cleanup_environment(self) -> None:
|
|
111
|
+
"""Clean up the virtual environment."""
|
|
112
|
+
# Note: We intentionally don't clean up the venv here to allow
|
|
113
|
+
# tools to continue using it after the context exits.
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def get_executable(self) -> str:
|
|
117
|
+
"""Get the path to the Python executable in the virtual environment."""
|
|
118
|
+
if not self._executable or not os.path.exists(self._executable):
|
|
119
|
+
raise RuntimeError("Virtual environment Python executable not found")
|
|
120
|
+
return self._executable
|
|
121
|
+
|
|
122
|
+
async def execute_code(self, code: str, script_path: str, timeout: int = 30) -> dict[str, Any]:
|
|
123
|
+
"""Execute code in the virtual environment."""
|
|
124
|
+
try:
|
|
125
|
+
# Get the Python executable
|
|
126
|
+
python_executable = self.get_executable()
|
|
127
|
+
|
|
128
|
+
# Verify the executable exists
|
|
129
|
+
if not os.path.exists(python_executable):
|
|
130
|
+
return {"success": False, "error": f"Python executable not found at {python_executable}"}
|
|
131
|
+
|
|
132
|
+
# Ensure the directory for the script exists
|
|
133
|
+
script_dir = os.path.dirname(script_path)
|
|
134
|
+
if script_dir:
|
|
135
|
+
os.makedirs(script_dir, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
# Write the code to the script file using asyncify (from base class)
|
|
138
|
+
await asyncify(self._write_to_file)(script_path, code)
|
|
139
|
+
|
|
140
|
+
logging.info(f"Wrote code to {script_path}")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
# Execute directly with subprocess using asyncify for better reliability
|
|
144
|
+
result = await asyncify(self._run_subprocess)([python_executable, script_path], timeout)
|
|
145
|
+
|
|
146
|
+
# Main execution result
|
|
147
|
+
return {
|
|
148
|
+
"success": result.returncode == 0,
|
|
149
|
+
"stdout": result.stdout,
|
|
150
|
+
"stderr": result.stderr,
|
|
151
|
+
"returncode": result.returncode,
|
|
152
|
+
}
|
|
153
|
+
except subprocess.TimeoutExpired:
|
|
154
|
+
return {"success": False, "error": f"Execution timed out after {timeout} seconds"}
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return {"success": False, "error": f"Execution error: {str(e)}"}
|
|
158
|
+
|
|
159
|
+
def _get_python_executable_for_version(self) -> str:
|
|
160
|
+
"""Get the Python executable for the specified version and verify it can create a venv."""
|
|
161
|
+
# If a specific path is provided, use it directly
|
|
162
|
+
if self.python_path:
|
|
163
|
+
if not os.path.exists(self.python_path) or not os.access(self.python_path, os.X_OK):
|
|
164
|
+
raise RuntimeError(f"Python executable not found at {self.python_path}")
|
|
165
|
+
return self.python_path
|
|
166
|
+
|
|
167
|
+
# If no specific version is requested, use the current Python
|
|
168
|
+
if not self.python_version:
|
|
169
|
+
return sys.executable
|
|
170
|
+
|
|
171
|
+
potential_executables = []
|
|
172
|
+
|
|
173
|
+
# Try to find a specific Python version using pyenv if available
|
|
174
|
+
try:
|
|
175
|
+
pyenv_result = subprocess.run(
|
|
176
|
+
["pyenv", "which", f"python{self.python_version}"],
|
|
177
|
+
check=True,
|
|
178
|
+
stdout=subprocess.PIPE,
|
|
179
|
+
stderr=subprocess.PIPE,
|
|
180
|
+
text=True,
|
|
181
|
+
)
|
|
182
|
+
potential_executables.append(pyenv_result.stdout.strip())
|
|
183
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
# Try common system paths based on platform
|
|
187
|
+
if os.name == "nt": # Windows
|
|
188
|
+
potential_executables.extend([
|
|
189
|
+
f"C:\\Python{self.python_version.replace('.', '')}\\python.exe",
|
|
190
|
+
f"C:\\Program Files\\Python{self.python_version.replace('.', '')}\\python.exe",
|
|
191
|
+
f"C:\\Program Files (x86)\\Python{self.python_version.replace('.', '')}\\python.exe",
|
|
192
|
+
])
|
|
193
|
+
else: # Unix-like (Mac and Linux)
|
|
194
|
+
# Add more paths that might exist on macOS
|
|
195
|
+
potential_executables.extend([
|
|
196
|
+
f"/usr/bin/python{self.python_version}",
|
|
197
|
+
f"/usr/local/bin/python{self.python_version}",
|
|
198
|
+
f"/opt/homebrew/bin/python{self.python_version}", # Homebrew on Apple Silicon
|
|
199
|
+
f"/opt/python/bin/python{self.python_version}",
|
|
200
|
+
])
|
|
201
|
+
|
|
202
|
+
# Try each potential path and verify it can create a venv
|
|
203
|
+
for path in potential_executables:
|
|
204
|
+
if os.path.exists(path) and os.access(path, os.X_OK):
|
|
205
|
+
# Verify this Python can create a venv
|
|
206
|
+
try:
|
|
207
|
+
test_result = subprocess.run(
|
|
208
|
+
[path, "-m", "venv", "--help"],
|
|
209
|
+
check=False, # Don't raise exception
|
|
210
|
+
stdout=subprocess.PIPE,
|
|
211
|
+
stderr=subprocess.PIPE,
|
|
212
|
+
text=True,
|
|
213
|
+
timeout=5, # Add timeout for safety
|
|
214
|
+
)
|
|
215
|
+
if test_result.returncode == 0:
|
|
216
|
+
# Successfully found a valid Python executable
|
|
217
|
+
return path
|
|
218
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
219
|
+
continue
|
|
220
|
+
|
|
221
|
+
# If we couldn't find the specified version, raise an exception
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
f"Python {self.python_version} not found or cannot create virtual environments. Provide a python_path to use a specific Python executable."
|
|
224
|
+
)
|
|
@@ -0,0 +1,75 @@
|
|
|
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 contextlib
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
__all__ = ["WorkingDirectory"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WorkingDirectory:
|
|
16
|
+
"""Context manager for changing the current working directory."""
|
|
17
|
+
|
|
18
|
+
_current_working_directory: ContextVar["WorkingDirectory"] = ContextVar("_current_working_directory")
|
|
19
|
+
|
|
20
|
+
def __init__(self, path: str):
|
|
21
|
+
"""
|
|
22
|
+
Initialize with a directory path.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
path: The directory path to change to.
|
|
26
|
+
"""
|
|
27
|
+
self.path = path
|
|
28
|
+
self.original_path = None
|
|
29
|
+
self.created_tmp = False
|
|
30
|
+
self._token = None
|
|
31
|
+
|
|
32
|
+
def __enter__(self):
|
|
33
|
+
"""Change to the specified directory and return self."""
|
|
34
|
+
self.original_path = os.getcwd()
|
|
35
|
+
if self.path:
|
|
36
|
+
os.makedirs(self.path, exist_ok=True)
|
|
37
|
+
os.chdir(self.path)
|
|
38
|
+
|
|
39
|
+
# Set this as the current working directory in the context
|
|
40
|
+
self._token = WorkingDirectory._current_working_directory.set(self)
|
|
41
|
+
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
45
|
+
"""Change back to the original directory and clean up if necessary."""
|
|
46
|
+
# Reset the context variable if this was the active working directory
|
|
47
|
+
if self._token is not None:
|
|
48
|
+
WorkingDirectory._current_working_directory.reset(self._token)
|
|
49
|
+
self._token = None
|
|
50
|
+
|
|
51
|
+
if self.original_path:
|
|
52
|
+
os.chdir(self.original_path)
|
|
53
|
+
if self.created_tmp and self.path and os.path.exists(self.path):
|
|
54
|
+
with contextlib.suppress(Exception):
|
|
55
|
+
shutil.rmtree(self.path)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def create_tmp(cls):
|
|
59
|
+
"""Create a temporary directory and return a WorkingDirectory instance for it."""
|
|
60
|
+
tmp_dir = tempfile.mkdtemp(prefix="ag2_work_dir_")
|
|
61
|
+
instance = cls(tmp_dir)
|
|
62
|
+
instance.created_tmp = True
|
|
63
|
+
return instance
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def get_current_working_directory(
|
|
67
|
+
cls, working_directory: Optional["WorkingDirectory"] = None
|
|
68
|
+
) -> Optional["WorkingDirectory"]:
|
|
69
|
+
"""Get the current working directory or the specified one if provided."""
|
|
70
|
+
if working_directory is not None:
|
|
71
|
+
return working_directory
|
|
72
|
+
try:
|
|
73
|
+
return cls._current_working_directory.get()
|
|
74
|
+
except LookupError:
|
|
75
|
+
return None
|
autogen/llm_config.py
CHANGED
|
@@ -9,7 +9,7 @@ from abc import ABC, abstractmethod
|
|
|
9
9
|
from collections.abc import Iterable
|
|
10
10
|
from contextvars import ContextVar
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import TYPE_CHECKING, Annotated, Any, Mapping, Optional, Type, TypeVar, Union
|
|
12
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, Mapping, Optional, Type, TypeVar, Union
|
|
13
13
|
|
|
14
14
|
from httpx import Client as httpxClient
|
|
15
15
|
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, SecretStr, ValidationInfo, field_serializer, field_validator
|
|
@@ -268,6 +268,7 @@ class LLMConfig(metaclass=MetaLLMConfig):
|
|
|
268
268
|
list[Annotated[Union[llm_config_classes], Field(discriminator="api_type")]],
|
|
269
269
|
Field(default_factory=list, min_length=1),
|
|
270
270
|
]
|
|
271
|
+
routing_method: Optional[Literal["fixed_order", "round_robin"]] = None
|
|
271
272
|
|
|
272
273
|
# Following field is configuration for pydantic to disallow extra fields
|
|
273
274
|
model_config = ConfigDict(extra="forbid")
|
|
@@ -302,13 +303,15 @@ class LLMConfigEntry(BaseModel, ABC):
|
|
|
302
303
|
@field_validator("base_url", mode="before")
|
|
303
304
|
@classmethod
|
|
304
305
|
def check_base_url(cls, v: Any, info: ValidationInfo) -> Any:
|
|
306
|
+
if v is None: # Handle None case explicitly
|
|
307
|
+
return None
|
|
305
308
|
if not str(v).startswith("https://") and not str(v).startswith("http://"):
|
|
306
309
|
v = f"http://{str(v)}"
|
|
307
310
|
return v
|
|
308
311
|
|
|
309
|
-
@field_serializer("base_url")
|
|
312
|
+
@field_serializer("base_url", when_used="unless-none") # Ensure serializer also respects None
|
|
310
313
|
def serialize_base_url(self, v: Any) -> Any:
|
|
311
|
-
return str(v)
|
|
314
|
+
return str(v) if v is not None else None
|
|
312
315
|
|
|
313
316
|
@field_serializer("api_key", when_used="unless-none")
|
|
314
317
|
def serialize_api_key(self, v: SecretStr) -> Any:
|
autogen/logger/sqlite_logger.py
CHANGED
|
@@ -129,12 +129,12 @@ class MCPProxy:
|
|
|
129
129
|
return mcp
|
|
130
130
|
|
|
131
131
|
def _process_params(
|
|
132
|
-
self,
|
|
132
|
+
self, process_path: str, func: Callable[[Any], Any], **kwargs: Any
|
|
133
133
|
) -> tuple[str, dict[str, Any], dict[str, Any]]:
|
|
134
|
-
|
|
135
|
-
q_params, path_params, body, security = MCPProxy._get_params(
|
|
134
|
+
process_path = MCPProxy._convert_camel_case_within_braces_to_snake(process_path)
|
|
135
|
+
q_params, path_params, body, security = MCPProxy._get_params(process_path, func)
|
|
136
136
|
|
|
137
|
-
expanded_path =
|
|
137
|
+
expanded_path = process_path.format(**{p: kwargs[p] for p in path_params})
|
|
138
138
|
|
|
139
139
|
url = self._servers[0]["url"] + expanded_path
|
|
140
140
|
|
autogen/oai/client.py
CHANGED
|
@@ -246,6 +246,12 @@ class OpenAILLMConfigEntry(LLMConfigEntry):
|
|
|
246
246
|
price: Optional[list[float]] = Field(default=None, min_length=2, max_length=2)
|
|
247
247
|
tool_choice: Optional[Literal["none", "auto", "required"]] = None
|
|
248
248
|
user: Optional[str] = None
|
|
249
|
+
|
|
250
|
+
# ⏺ The extra_body parameter flows from OpenAILLMConfigEntry to the LLM request through this path:
|
|
251
|
+
# 1. Config Definition: extra_body is defined in OpenAILLMConfigEntry (autogen/oai/client.py:248)
|
|
252
|
+
# 2. Parameter Classification: It's classified as an OpenAI client parameter (not AG2-specific) via the openai_kwargs property (autogen/oai/client.py:752-758)
|
|
253
|
+
# 3. Request Separation: In _separate_create_config() (autogen/oai/client.py:842), extra_body goes into create_config since it's not in the extra_kwargs set.
|
|
254
|
+
# 4. API Call: The create_config becomes params and gets passed directly to OpenAI's create() method via **params (autogen/oai/client.py:551,658)
|
|
249
255
|
extra_body: Optional[dict[str, Any]] = (
|
|
250
256
|
None # For VLLM - See here: https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html#extra-parameters
|
|
251
257
|
)
|
|
@@ -806,17 +812,29 @@ class OpenAIWrapper:
|
|
|
806
812
|
self._clients: list[ModelClient] = []
|
|
807
813
|
self._config_list: list[dict[str, Any]] = []
|
|
808
814
|
|
|
815
|
+
# Determine routing_method from base_config only.
|
|
816
|
+
self.routing_method = base_config.get("routing_method") or "fixed_order"
|
|
817
|
+
self._round_robin_index = 0
|
|
818
|
+
|
|
819
|
+
# Remove routing_method from extra_kwargs after it has been used to set self.routing_method
|
|
820
|
+
# This ensures it's not part of the individual client configurations that are based on extra_kwargs.
|
|
821
|
+
extra_kwargs.pop("routing_method", None)
|
|
822
|
+
|
|
809
823
|
if config_list:
|
|
810
824
|
config_list = [config.copy() for config in config_list] # make a copy before modifying
|
|
811
|
-
for
|
|
812
|
-
self._register_default_client(
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}
|
|
825
|
+
for config_item in config_list:
|
|
826
|
+
self._register_default_client(config_item, openai_config)
|
|
827
|
+
# Construct current_config_extra_kwargs using the cleaned extra_kwargs
|
|
828
|
+
# (which doesn't have routing_method from base_config)
|
|
829
|
+
# and specific non-openai kwargs from config_item.
|
|
830
|
+
config_item_specific_extras = {k: v for k, v in config_item.items() if k not in self.openai_kwargs}
|
|
831
|
+
self._config_list.append({**extra_kwargs, **config_item_specific_extras})
|
|
817
832
|
else:
|
|
833
|
+
# For a single config passed via base_config (already in extra_kwargs)
|
|
818
834
|
self._register_default_client(extra_kwargs, openai_config)
|
|
835
|
+
# extra_kwargs has already had routing_method popped.
|
|
819
836
|
self._config_list = [extra_kwargs]
|
|
837
|
+
|
|
820
838
|
self.wrapper_id = id(self)
|
|
821
839
|
|
|
822
840
|
def _separate_openai_config(self, config: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
@@ -1074,7 +1092,16 @@ class OpenAIWrapper:
|
|
|
1074
1092
|
raise RuntimeError(
|
|
1075
1093
|
f"Model client(s) {non_activated} are not activated. Please register the custom model clients using `register_model_client` or filter them out form the config list."
|
|
1076
1094
|
)
|
|
1077
|
-
|
|
1095
|
+
|
|
1096
|
+
ordered_clients_indices = list(range(len(self._clients)))
|
|
1097
|
+
if self.routing_method == "round_robin" and len(self._clients) > 0:
|
|
1098
|
+
ordered_clients_indices = (
|
|
1099
|
+
ordered_clients_indices[self._round_robin_index :] + ordered_clients_indices[: self._round_robin_index]
|
|
1100
|
+
)
|
|
1101
|
+
self._round_robin_index = (self._round_robin_index + 1) % len(self._clients)
|
|
1102
|
+
|
|
1103
|
+
for i in ordered_clients_indices:
|
|
1104
|
+
client = self._clients[i]
|
|
1078
1105
|
# merge the input config with the i-th config in the config list
|
|
1079
1106
|
full_config = {**config, **self._config_list[i]}
|
|
1080
1107
|
# separate the config into create_config and extra_kwargs
|
autogen/oai/gemini.py
CHANGED
|
@@ -126,6 +126,7 @@ class GeminiClient:
|
|
|
126
126
|
PARAMS_MAPPING = {
|
|
127
127
|
"max_tokens": "max_output_tokens",
|
|
128
128
|
# "n": "candidate_count", # Gemini supports only `n=1`
|
|
129
|
+
"seed": "seed",
|
|
129
130
|
"stop_sequences": "stop_sequences",
|
|
130
131
|
"temperature": "temperature",
|
|
131
132
|
"top_p": "top_p",
|