import importlib.abc import importlib.util import subprocess import sys import textwrap from pathlib import Path import agency_swarm from agency_swarm import Agency, Agent from agency_swarm.agency.helpers import run_fastapi as helpers_run_fastapi from agency_swarm.tools import SendMessage class _BlockOptionalDepsFinder(importlib.abc.MetaPathFinder): def find_spec(self, fullname: str, path, target=None): # noqa: ANN001, ANN201 if fullname in {"fastapi", "uvicorn"}: raise ModuleNotFoundError(fullname) if fullname.startswith("fastapi.") or fullname.startswith("uvicorn."): raise ModuleNotFoundError(fullname) return None def test_integrations_fastapi_imports_without_optional_dependencies(caplog): """`agency_swarm.integrations.fastapi` must import without the fastapi extra installed.""" fastapi_module_path = Path(agency_swarm.__file__).resolve().parent / "integrations" / "fastapi.py" spec = importlib.util.spec_from_file_location("agency_swarm_test_fastapi_no_deps", fastapi_module_path) assert spec is not None assert spec.loader is not None module = importlib.util.module_from_spec(spec) blocker = _BlockOptionalDepsFinder() saved_fastapi = sys.modules.pop("fastapi", None) saved_uvicorn = sys.modules.pop("uvicorn", None) sys.meta_path.insert(0, blocker) try: spec.loader.exec_module(module) caplog.set_level("ERROR") module.run_fastapi(agencies={"test": lambda **_: None}) finally: sys.meta_path.remove(blocker) if saved_fastapi is not None: sys.modules["fastapi"] = saved_fastapi if saved_uvicorn is not None: sys.modules["uvicorn"] = saved_uvicorn assert "FastAPI deployment dependencies are missing" in caplog.text def test_run_fastapi_creates_new_agency_instance(mocker): agent = Agent(name="HelperAgent", instructions="test", model="gpt-5.4-mini") agency = Agency(agent) captured = {} def fake_run_fastapi(*, agencies=None, **kwargs): captured["factory"] = agencies["agency"] return None mocker.patch("agency_swarm.integrations.fastapi.run_fastapi", side_effect=fake_run_fastapi) helpers_run_fastapi(agency) factory = captured["factory"] load_called = False def load_cb(): nonlocal load_called load_called = True return [] new_agency = factory(load_threads_callback=load_cb) assert load_called, "load_threads_callback was not invoked" assert new_agency is not agency, "Factory should create a new Agency instance" class CustomSendMessage(SendMessage): """Test-specific send_message tool.""" def test_run_fastapi_preserves_custom_tool_mappings(mocker): sender = Agent(name="A", instructions="test", model="gpt-5.4-mini") recipient = Agent(name="B", instructions="test", model="gpt-5.4-mini") agency = Agency(sender, recipient, communication_flows=[(sender, recipient, CustomSendMessage)]) captured = {} def fake_run_fastapi(*, agencies=None, **kwargs): captured["factory"] = agencies["agency"] return None mocker.patch("agency_swarm.integrations.fastapi.run_fastapi", side_effect=fake_run_fastapi) helpers_run_fastapi(agency) factory = captured["factory"] new_agency = factory() pair = ("A", "B") assert new_agency._communication_tool_classes.get(pair) is CustomSendMessage, ( "Custom tool mapping was not preserved" ) def test_run_fastapi_normalizes_relative_shared_folders_for_factory_calls(mocker, tmp_path: Path): """Relative shared_*_folder must be stable across agency_factory call stacks. The FastAPI integration calls agency_factory from within the server stack (uvicorn/fastapi), which changes get_external_caller_directory(). We normalize relative shared folders to absolute once when run_fastapi is called, so the rebuilt Agency can still load shared resources. """ creator_dir = tmp_path / "creator" creator_dir.mkdir() shared_tools_dir = creator_dir / "shared_tools" shared_tools_dir.mkdir() (shared_tools_dir / "SampleTool.py").write_text( textwrap.dedent( """ from agency_swarm.tools import BaseTool from pydantic import Field class SampleTool(BaseTool): \"\"\"A sample tool.\"\"\" message: str = Field(..., description="Message to echo") def run(self) -> str: return f"Echo: {self.message}" """ ).strip() + "\n" ) captured: dict[str, object] = {} def fake_run_fastapi(*, agencies=None, **_kwargs): captured["factory"] = agencies["agency"] return None mocker.patch("agency_swarm.integrations.fastapi.run_fastapi", side_effect=fake_run_fastapi) creator_code = textwrap.dedent( """ from agency_swarm import Agency, Agent from agency_swarm.agency.helpers import run_fastapi as helpers_run_fastapi a = Agent(name="A", instructions="test", model="gpt-5.4-mini") agency = Agency(a, shared_tools_folder="shared_tools") helpers_run_fastapi(agency) """ ).strip() exec(compile(creator_code, str(creator_dir / "create_agency.py"), "exec"), {}) factory = captured["factory"] assert callable(factory) other_dir = tmp_path / "other" other_dir.mkdir() call_code = textwrap.dedent( """ agency2 = factory() agent = agency2.agents["A"] tool_names = [getattr(t, "name", None) for t in agent.tools] """ ).strip() ns = {"factory": factory} exec(compile(call_code, str(other_dir / "call_factory.py"), "exec"), ns) assert "SampleTool" in ns["tool_names"] def test_package_star_import_succeeds_without_jupyter_dependencies() -> None: """`from agency_swarm import *` should not fail when jupyter extras are missing.""" script = textwrap.dedent( """ import builtins import importlib.util original_find_spec = importlib.util.find_spec original_import = builtins.__import__ def blocked_find_spec(name, package=None): if name == "jupyter_client": return None return original_find_spec(name, package) def blocked_import(name, globals=None, locals=None, fromlist=(), level=0): if name == "jupyter_client" or name.startswith("jupyter_client."): raise ModuleNotFoundError(name) return original_import(name, globals, locals, fromlist, level) importlib.util.find_spec = blocked_find_spec builtins.__import__ = blocked_import namespace = {} exec("from agency_swarm import *", namespace) assert "IPythonInterpreter" not in namespace """ ) result = subprocess.run([sys.executable, "-c", script], capture_output=True, text=True, check=False) assert result.returncode == 0, result.stderr or result.stdout