diff --git a/src/smallestai/atoms/crew/__init__.py b/src/smallestai/atoms/crew/__init__.py index e63509ac..fbd176f0 100644 --- a/src/smallestai/atoms/crew/__init__.py +++ b/src/smallestai/atoms/crew/__init__.py @@ -5,9 +5,50 @@ chatbots, and other interactive AI applications. Usage: - from smallestai.atoms.crew.server import AtomsCrewApp - from smallestai.atoms.crew.session import CrewSession - from smallestai.atoms.crew.nodes import OutputCrewNode, BackgroundCrewNode - from smallestai.atoms.crew.clients.openai import OpenAIClient - from smallestai.atoms.crew.tools import function_tool, ToolRegistry + from smallestai.atoms.crew import AtomsCrewApp + from smallestai.atoms.crew import CrewSession + from smallestai.atoms.crew import OutputCrewNode, BackgroundCrewNode + from smallestai.atoms.crew import OpenAIClient + from smallestai.atoms.crew import function_tool, ToolRegistry + +The submodule paths (e.g. ``smallestai.atoms.crew.server``) also work. """ + +from typing import TYPE_CHECKING + +# Public API. Imported lazily via __getattr__ (PEP 562) so that importing this +# package does not eagerly pull in the crew runtime deps (fastapi, uvicorn, ...) +# that `server` and friends need. Access a name and it is imported on demand. +_LAZY_EXPORTS = { + "AtomsCrewApp": "smallestai.atoms.crew.server", + "CrewSession": "smallestai.atoms.crew.session", + "OutputCrewNode": "smallestai.atoms.crew.nodes", + "BackgroundCrewNode": "smallestai.atoms.crew.nodes", + "OpenAIClient": "smallestai.atoms.crew.clients.openai", + "function_tool": "smallestai.atoms.crew.tools", + "ToolRegistry": "smallestai.atoms.crew.tools", +} + +__all__ = list(_LAZY_EXPORTS.keys()) + +if TYPE_CHECKING: + from smallestai.atoms.crew.clients.openai import OpenAIClient as OpenAIClient + from smallestai.atoms.crew.nodes import BackgroundCrewNode as BackgroundCrewNode + from smallestai.atoms.crew.nodes import OutputCrewNode as OutputCrewNode + from smallestai.atoms.crew.server import AtomsCrewApp as AtomsCrewApp + from smallestai.atoms.crew.session import CrewSession as CrewSession + from smallestai.atoms.crew.tools import ToolRegistry as ToolRegistry + from smallestai.atoms.crew.tools import function_tool as function_tool + + +def __getattr__(name: str): + module_path = _LAZY_EXPORTS.get(name) + if module_path is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + import importlib + + return getattr(importlib.import_module(module_path), name) + + +def __dir__(): + return sorted(list(globals().keys()) + __all__) diff --git a/tests/custom/test_public_imports.py b/tests/custom/test_public_imports.py new file mode 100644 index 00000000..2e46aeb6 --- /dev/null +++ b/tests/custom/test_public_imports.py @@ -0,0 +1,57 @@ +"""Guards the documented public import surface of `smallestai.atoms.crew`. + +A customer hit `ImportError` on `from smallestai.atoms.crew import AtomsCrewApp` +because `crew/__init__` exported nothing (the imports there lived only in the +docstring). Both the module docstring and the docs advertise this short path, so +lock it here: if the package stops exporting a public symbol, this test fails in +CI rather than in a customer's notebook. +""" + +import importlib + +CREW_PUBLIC = [ + "AtomsCrewApp", + "CrewSession", + "OutputCrewNode", + "BackgroundCrewNode", + "OpenAIClient", + "function_tool", + "ToolRegistry", +] + + +def test_crew_all_advertises_public_api(): + mod = importlib.import_module("smallestai.atoms.crew") + assert set(CREW_PUBLIC) <= set(mod.__all__) + + +def test_crew_public_symbols_resolve_from_short_path(): + from smallestai.atoms.crew import ( # noqa: F401 + AtomsCrewApp, + BackgroundCrewNode, + CrewSession, + OpenAIClient, + OutputCrewNode, + ToolRegistry, + function_tool, + ) + + for sym in ( + AtomsCrewApp, + CrewSession, + OutputCrewNode, + BackgroundCrewNode, + OpenAIClient, + ToolRegistry, + function_tool, + ): + assert sym is not None + + +def test_unknown_attr_raises_attributeerror(): + mod = importlib.import_module("smallestai.atoms.crew") + try: + mod.DefinitelyNotAThing + except AttributeError: + return + raise AssertionError("expected AttributeError for an unknown attribute")