| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- 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
|