File size: 5,531 Bytes
9cad5fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70f686d
09fbc82
 
9cad5fb
 
 
09fbc82
9cad5fb
 
 
70f686d
9cad5fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70f686d
9cad5fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70f686d
 
 
9cad5fb
 
70f686d
9cad5fb
 
 
 
 
 
 
 
 
 
 
 
 
 
70f686d
 
09fbc82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70f686d
 
 
 
 
 
 
 
 
 
 
 
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
import sys
import importlib
from types import ModuleType
from pathlib import Path

import pytest

import reachy_mini_conversation_app.config as config_mod


def _reload_core_tools() -> ModuleType:
    """Reload core_tools after config object has been patched."""
    for module_name in list(sys.modules):
        if module_name.startswith("reachy_mini_conversation_app.tools."):
            sys.modules.pop(module_name, None)
    # External file-loaded modules are registered by bare tool name.
    sys.modules.pop("ext_ping", None)
    sys.modules.pop("sweep_look", None)
    sys.modules.pop("ext_dup_a", None)
    sys.modules.pop("ext_dup_b", None)

    sys.modules.pop("reachy_mini_conversation_app.tools.core_tools", None)
    core_tools_mod = importlib.import_module("reachy_mini_conversation_app.tools.core_tools")
    core_tools_mod.initialize_tools()
    return core_tools_mod


def test_external_profile_can_use_builtin_tools(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """External profile tools.txt can reference built-in src tools."""
    profile_name = "ext_profile_test"
    external_profiles_root = tmp_path / "external_profiles"
    profile_dir = external_profiles_root / profile_name
    profile_dir.mkdir(parents=True)
    (profile_dir / "instructions.txt").write_text("hello\n", encoding="utf-8")
    (profile_dir / "tools.txt").write_text("dance\n", encoding="utf-8")

    monkeypatch.setattr(config_mod.config, "REACHY_MINI_CUSTOM_PROFILE", profile_name)
    monkeypatch.setattr(config_mod.config, "PROFILES_DIRECTORY", external_profiles_root)
    monkeypatch.setattr(config_mod.config, "TOOLS_DIRECTORY", None)
    monkeypatch.setattr(config_mod.config, "AUTOLOAD_EXTERNAL_TOOLS", False)

    core_tools_mod = _reload_core_tools()

    assert "dance" in core_tools_mod.ALL_TOOLS
    assert "dance" not in sys.modules


def test_external_tools_can_be_loaded_without_external_profile(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    """External tools can be loaded with built-in profile via autoload mode."""
    external_tools_root = tmp_path / "external_tools"
    external_tools_root.mkdir(parents=True)

    (external_tools_root / "ext_ping.py").write_text(
        "\n".join(
            [
                "from typing import Any, Dict",
                "from reachy_mini_conversation_app.tools.core_tools import Tool, ToolDependencies",
                "",
                "class ExtPingTool(Tool):",
                '    name = "ext_ping"',
                '    description = "External ping tool"',
                '    parameters_schema = {"type": "object", "properties": {}, "required": []}',
                "",
                "    async def __call__(self, deps: ToolDependencies, **kwargs: Any) -> Dict[str, Any]:",
                '        return {"status": "ok"}',
                "",
            ]
        ),
        encoding="utf-8",
    )

    monkeypatch.setattr(config_mod.config, "REACHY_MINI_CUSTOM_PROFILE", "default")
    monkeypatch.setattr(config_mod.config, "PROFILES_DIRECTORY", config_mod.DEFAULT_PROFILES_DIRECTORY)
    monkeypatch.setattr(config_mod.config, "TOOLS_DIRECTORY", external_tools_root)
    monkeypatch.setattr(config_mod.config, "AUTOLOAD_EXTERNAL_TOOLS", True)

    core_tools_mod = _reload_core_tools()

    assert "ext_ping" in core_tools_mod.ALL_TOOLS


def test_external_tools_fail_on_duplicate_tool_names(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
    """Loading must fail if multiple tools declare the same Tool.name."""
    external_tools_root = tmp_path / "external_tools"
    external_tools_root.mkdir(parents=True)

    duplicate_tool_source = "\n".join(
        [
            "from typing import Any, Dict",
            "from reachy_mini_conversation_app.tools.core_tools import Tool, ToolDependencies",
            "",
            "class DupTool(Tool):",
            '    name = "dup_tool"',
            '    description = "Duplicate tool name"',
            '    parameters_schema = {"type": "object", "properties": {}, "required": []}',
            "",
            "    async def __call__(self, deps: ToolDependencies, **kwargs: Any) -> Dict[str, Any]:",
            '        return {"status": "ok"}',
            "",
        ]
    )
    (external_tools_root / "ext_dup_a.py").write_text(duplicate_tool_source, encoding="utf-8")
    (external_tools_root / "ext_dup_b.py").write_text(duplicate_tool_source, encoding="utf-8")

    monkeypatch.setattr(config_mod.config, "REACHY_MINI_CUSTOM_PROFILE", "default")
    monkeypatch.setattr(config_mod.config, "PROFILES_DIRECTORY", config_mod.DEFAULT_PROFILES_DIRECTORY)
    monkeypatch.setattr(config_mod.config, "TOOLS_DIRECTORY", external_tools_root)
    monkeypatch.setattr(config_mod.config, "AUTOLOAD_EXTERNAL_TOOLS", True)

    with pytest.raises(RuntimeError, match="Duplicate Tool.name values detected"):
        _reload_core_tools()


def test_builtin_profile_can_load_profile_local_tools(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Built-in profile-local tools should load from the packaged profiles root."""
    monkeypatch.setattr(config_mod.config, "REACHY_MINI_CUSTOM_PROFILE", "example")
    monkeypatch.setattr(config_mod.config, "PROFILES_DIRECTORY", config_mod.DEFAULT_PROFILES_DIRECTORY)
    monkeypatch.setattr(config_mod.config, "TOOLS_DIRECTORY", None)
    monkeypatch.setattr(config_mod.config, "AUTOLOAD_EXTERNAL_TOOLS", False)

    core_tools_mod = _reload_core_tools()

    assert "sweep_look" in core_tools_mod.ALL_TOOLS