Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest playwright pytest-asyncio
pip install pytest playwright pytest-asyncio pytest-cov baidusearch
pip install ruff docformatter
pip install -r requirements.txt

Expand All @@ -36,4 +36,4 @@ jobs:

- name: Run Unit Tests
run: |
pytest test/unittest
pytest test/unittest --cov=oxygent --cov-report=term-missing:skip-covered --cov-report=xml
2 changes: 1 addition & 1 deletion oxygent/preset_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,4 @@
print(f"Warning: Failed to import tool '{module_name}': {error_msg}")

# Set the module entry to None to prevent errors in subsequent use
globals()[module_name] = None
globals()[module_name] = None
39 changes: 39 additions & 0 deletions oxygent/preset_tools/python_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import logging
from typing import Optional

from oxygent.oxy import FunctionHub

logger = logging.getLogger(__name__)
python_tools = FunctionHub(name="python_tools")


@python_tools.tool(
description="Runs Python code in the current environment."
)
def run_python_code(
code: str,
variable_to_return: Optional[str] = None,
safe_globals: Optional[dict] = None,
safe_locals: Optional[dict] = None
) -> str:
try:
logger.debug(f"Running code:\n\n{code}\n\n")
if not safe_globals:
safe_globals = globals()
if not safe_locals:
safe_locals = locals()

exec(code, safe_globals, safe_locals)

if variable_to_return:
variable_value = safe_locals.get(variable_to_return)
if variable_value is None:
return f"Variable {variable_to_return} not found"
logger.debug(
f"Variable {variable_to_return} value: {variable_value}")
return str(variable_value)
else:
return "successfully run python code"
except Exception as e:
logger.error(f"Error running python code: {e}")
return f"Error running python code: {e}"
37 changes: 37 additions & 0 deletions oxygent/preset_tools/shell_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import logging
import subprocess
from typing import List, Optional

from oxygent.oxy import FunctionHub
from pydantic import Field

logger = logging.getLogger(__name__)
shell_tools = FunctionHub(name="shell_tools")


@shell_tools.tool(
description="Run a shell command and return the output or error."
)
def run_shell_command(
args: List[str] = Field(description="command arguments"),
tail: int = 10,
base_dir: Optional[str] = None
) -> str:
"""Runs a shell command and returns the output or error."""

try:
logger.info(f"Running shell command: {args}")
result = subprocess.run(
args,
capture_output=True,
encoding="utf8",
shell=True,
text=True,
cwd=base_dir
)
if result.returncode != 0:
return f"Error: {result.stderr}"
return "\n".join(result.stdout.split("\n")[-tail:])
except Exception as e:
logger.warning(f"Failed to run shell command: {e}")
return f"Error: {e}"
45 changes: 45 additions & 0 deletions test/unittest/test_tool/test_python_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import pytest

from oxygent.preset_tools.python_tools import run_python_code


@pytest.mark.asyncio
async def test_simple_code_execution():
code = "result = 2 + 3"
output = await run_python_code(code, variable_to_return="result")
assert output == "5"
code = "x = 10"
output = await run_python_code(code)
assert output == "successfully run python code"
code = "x = 5"
output = await run_python_code(code, variable_to_return="y")
assert output == "Variable y not found"


@pytest.mark.asyncio
async def test_error_handling():
code = "raise ValueError('Test error')"
output = await run_python_code(code)
assert "Error running python code" in output
assert "Test error" in output


@pytest.mark.asyncio
async def test_with_others():
code = "result = test_var * 2"
custom_globals = {"test_var": 10}
output = await run_python_code(
code, variable_to_return="result",
safe_globals=custom_globals)
assert output == "20"
code = "message = 'Hello World'"
output = await run_python_code(code, variable_to_return="message")

assert output == "Hello World"
code = "numbers = [1, 2, 3, 4, 5]"
output = await run_python_code(code, variable_to_return="numbers")
assert output == "[1, 2, 3, 4, 5]"

code = "flag = True"
output = await run_python_code(code, variable_to_return="flag")
assert output == "True"
147 changes: 147 additions & 0 deletions test/unittest/test_tool/test_shell_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import os
import pytest
import subprocess
import tempfile
from unittest.mock import patch, MagicMock

from oxygent.preset_tools.shell_tools import run_shell_command


@pytest.mark.asyncio
@patch('subprocess.run')
async def test_run_shell_command_success(mock_run):
"""Test successful shell command execution"""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "line1\nline2\nline3\nline4\nline5"
mock_run.return_value = mock_result

result = await run_shell_command(["echo", "test"])

mock_run.assert_called_once_with(
["echo", "test"],
capture_output=True,
encoding="utf8",
shell=True,
text=True,
cwd=None
)
assert result == "line1\nline2\nline3\nline4\nline5"


@pytest.mark.asyncio
@patch('subprocess.run')
async def test_run_shell_command_with_tail(mock_run):
"""Test shell command execution with custom tail value"""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "line1\nline2\nline3\nline4\nline5\nline6"
mock_run.return_value = mock_result

result = await run_shell_command(["echo", "test"], tail=3)

assert result == "line4\nline5\nline6"


@pytest.mark.asyncio
@patch('subprocess.run')
async def test_run_shell_command_with_base_dir(mock_run):
"""Test shell command execution with custom base directory"""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "success"
mock_run.return_value = mock_result

test_dir = "/tmp/test"
result = await run_shell_command(["pwd"], base_dir=test_dir)

mock_run.assert_called_once_with(
["pwd"],
capture_output=True,
encoding="utf8",
shell=True,
text=True,
cwd=test_dir
)
assert result == "success"


@pytest.mark.asyncio
@patch('subprocess.run')
async def test_run_shell_command_error(mock_run):
"""Test shell command execution with error"""
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stderr = "Command not found"
mock_run.return_value = mock_result

result = await run_shell_command(["nonexistent_command"])

assert result == "Error: Command not found"


@pytest.mark.asyncio
@patch('subprocess.run')
async def test_run_shell_command_exception(mock_run):
"""Test shell command execution with exception"""
mock_run.side_effect = Exception("Subprocess failed")

result = await run_shell_command(["test"])

assert result == "Error: Subprocess failed"


@pytest.mark.asyncio
@patch('subprocess.run')
async def test_run_shell_command_empty_output(mock_run):
"""Test shell command execution with empty output"""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = ""
mock_run.return_value = mock_result

result = await run_shell_command(["echo", "-n", ""])

assert result == ""


@pytest.mark.asyncio
@patch('subprocess.run')
async def test_run_shell_command_single_line(mock_run):
"""Test shell command execution with single line output"""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "single line"
mock_run.return_value = mock_result

result = await run_shell_command(["echo", "single line"])

assert result == "single line"


@pytest.mark.asyncio
@patch('subprocess.run')
async def test_run_shell_command_large_tail(mock_run):
"""Test shell command execution with tail larger than output lines"""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "line1\nline2"
mock_run.return_value = mock_result

result = await run_shell_command(["echo", "test"], tail=10)

assert result == "line1\nline2"


@pytest.mark.asyncio
@patch('subprocess.run')
async def test_run_shell_command_zero_tail(mock_run):
"""Test shell command execution with zero tail"""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "line1\nline2\nline3"
mock_run.return_value = mock_result

result = await run_shell_command(["echo", "test"], tail=0)

assert result == "line1\nline2\nline3"