-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix: implement lazy loading for tools imports to improve package startup time #1172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -59,18 +59,7 @@ | |||||
| # Import configuration (lightweight, no heavy deps) | ||||||
| from . import _config | ||||||
|
|
||||||
| # Lightweight imports that don't trigger heavy dependency chains | ||||||
| from .tools.tools import Tools | ||||||
| from .tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool | ||||||
| from .tools.decorator import tool, FunctionTool | ||||||
| from .tools.registry import get_registry, register_tool, get_tool, ToolRegistry | ||||||
| # db and obs are lazy-loaded via __getattr__ for performance | ||||||
|
|
||||||
| # Sub-packages for organized imports (pa.config, pa.tools, etc.) | ||||||
| # These enable: import praisonaiagents as pa; pa.config.MemoryConfig | ||||||
| from . import config | ||||||
| from . import tools | ||||||
| # Note: db, obs, knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps | ||||||
| # Note: tools, config, memory, workflows, db, obs, knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps | ||||||
|
|
||||||
| # Embedding API - LAZY LOADED via __getattr__ for performance | ||||||
| # Supports: embedding, embeddings, aembedding, aembeddings, EmbeddingResult, get_dimensions | ||||||
|
|
@@ -125,6 +114,19 @@ def _get_lazy_cache(): | |||||
| # ============================================================================ | ||||||
|
|
||||||
| _LAZY_IMPORTS = { | ||||||
| # Tools (moved from eager imports for lazy loading) | ||||||
| 'Tools': ('praisonaiagents.tools.tools', 'Tools'), | ||||||
| 'BaseTool': ('praisonaiagents.tools.base', 'BaseTool'), | ||||||
| 'ToolResult': ('praisonaiagents.tools.base', 'ToolResult'), | ||||||
| 'ToolValidationError': ('praisonaiagents.tools.base', 'ToolValidationError'), | ||||||
| 'validate_tool': ('praisonaiagents.tools.base', 'validate_tool'), | ||||||
| 'tool': ('praisonaiagents.tools.decorator', 'tool'), | ||||||
| 'FunctionTool': ('praisonaiagents.tools.decorator', 'FunctionTool'), | ||||||
| 'get_registry': ('praisonaiagents.tools.registry', 'get_registry'), | ||||||
| 'register_tool': ('praisonaiagents.tools.registry', 'register_tool'), | ||||||
| 'get_tool': ('praisonaiagents.tools.registry', 'get_tool'), | ||||||
| 'ToolRegistry': ('praisonaiagents.tools.registry', 'ToolRegistry'), | ||||||
|
Comment on lines
+117
to
+128
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Restore the full top-level tools surface before disabling fallback. Only a subset of Based on learnings: "Public API changes require a deprecation cycle: emit DeprecationWarning for one release before breaking change" Also applies to: 594-595 |
||||||
|
|
||||||
| # Main display utilities (imports rich) | ||||||
| 'TaskOutput': ('praisonaiagents.main', 'TaskOutput'), | ||||||
| 'ReflectionOutput': ('praisonaiagents.main', 'ReflectionOutput'), | ||||||
|
|
@@ -179,9 +181,7 @@ def _get_lazy_cache(): | |||||
| 'HandoffDepthError': ('praisonaiagents.agent.handoff', 'HandoffDepthError'), | ||||||
| 'HandoffTimeoutError': ('praisonaiagents.agent.handoff', 'HandoffTimeoutError'), | ||||||
|
|
||||||
| # Embedding API | ||||||
| 'embedding': ('praisonaiagents.embedding.embed', 'embedding'), | ||||||
| 'embeddings': ('praisonaiagents.embedding.embed', 'embedding'), | ||||||
| # Embedding API (Note: embedding/embeddings handled in custom_handler to override subpackage) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment here states that
Suggested change
|
||||||
| 'aembedding': ('praisonaiagents.embedding.embed', 'aembedding'), | ||||||
| 'aembeddings': ('praisonaiagents.embedding.embed', 'aembedding'), | ||||||
| 'embed': ('praisonaiagents.embedding.embed', 'embed'), | ||||||
|
|
@@ -506,21 +506,27 @@ def _custom_handler(name, cache): | |||||
| cache['Agents'] = value | ||||||
| return value | ||||||
|
|
||||||
| # Task removed in v4.0.0 - use Task instead | ||||||
| if name == "Task": | ||||||
| raise ImportError( | ||||||
| "Task has been removed in v4.0.0. Use Task instead.\n" | ||||||
| "Migration: Replace 'from praisonaiagents import Task' with 'from praisonaiagents import Task'\n" | ||||||
| "Task supports all Task features including action, handler, loop_over, etc." | ||||||
| ) | ||||||
|
|
||||||
| # Module imports (return the module itself) | ||||||
| if name == 'tools': | ||||||
| import importlib | ||||||
| mod = importlib.import_module('.tools', 'praisonaiagents') | ||||||
| cache['tools'] = mod | ||||||
| return mod | ||||||
| if name == 'config': | ||||||
| import importlib | ||||||
| mod = importlib.import_module('.config', 'praisonaiagents') | ||||||
| cache['config'] = mod | ||||||
| return mod | ||||||
| if name == 'memory': | ||||||
| import importlib | ||||||
| return importlib.import_module('.memory', 'praisonaiagents') | ||||||
| mod = importlib.import_module('.memory', 'praisonaiagents') | ||||||
| cache['memory'] = mod | ||||||
| return mod | ||||||
| if name == 'workflows': | ||||||
| import importlib | ||||||
| return importlib.import_module('.workflows', 'praisonaiagents') | ||||||
| mod = importlib.import_module('.workflows', 'praisonaiagents') | ||||||
| cache['workflows'] = mod | ||||||
| return mod | ||||||
|
|
||||||
| raise AttributeError(f"Not handled by custom_handler: {name}") | ||||||
|
|
||||||
|
|
@@ -532,20 +538,57 @@ def _custom_handler(name, cache): | |||||
| # Override them here to return the function instead of the module. | ||||||
| # ============================================================================ | ||||||
|
|
||||||
| # Override 'embedding' to return the function, not the subpackage | ||||||
| from .embedding.embed import embedding as _embedding_func | ||||||
| embedding = _embedding_func | ||||||
| # Override 'embedding' and 'embeddings' at module level to prevent subpackage import | ||||||
| # These need to be set after _LAZY_IMPORTS is defined but before __getattr__ is created | ||||||
| def _get_embedding_func(): | ||||||
| """Lazy getter for embedding function.""" | ||||||
| # Import with alias to avoid overwriting the module proxy | ||||||
| from .embedding.embed import embedding as _embedding_func | ||||||
| return _embedding_func | ||||||
|
Comment on lines
+543
to
+547
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify whether the embedding proxy survives after first invocation
python3 - <<'EOF'
import sys
# Import the package
import praisonaiagents as pa
print(f"Before call: type(pa.embedding) = {type(pa.embedding).__name__}")
print(f"Before call: pa.embedding is _EmbeddingProxy? = {type(pa.embedding).__name__ == '_EmbeddingProxy'}")
# Check if embedding is callable (proxy should be callable)
print(f"Before call: callable(pa.embedding) = {callable(pa.embedding)}")
# Now trigger the lazy load by accessing __signature__ (doesn't require actual API call)
try:
import inspect
sig = inspect.signature(pa.embedding)
print(f"After signature access: type(pa.embedding) = {type(pa.embedding).__name__}")
print(f"After signature access: callable(pa.embedding) = {callable(pa.embedding)}")
except Exception as e:
print(f"Error getting signature: {e}")
# Check what's in sys.modules
if 'praisonaiagents.embedding' in sys.modules:
print(f"sys.modules['praisonaiagents.embedding'] exists")
submod = sys.modules['praisonaiagents.embedding']
print(f" type: {type(submod)}")
# Final check
print(f"Final: type(pa.embedding) = {type(pa.embedding).__name__}")
EOFRepository: MervinPraison/PraisonAI Length of output: 194 🏁 Script executed: fd -t f "__init__.py" "praisonaiagents" | grep -E "praisonaiagents.*__init__\.py$"Repository: MervinPraison/PraisonAI Length of output: 169 🏁 Script executed: git ls-files "src/praisonai-agents/praisonaiagents/__init__.py"Repository: MervinPraison/PraisonAI Length of output: 115 🏁 Script executed: cat -n src/praisonai-agents/praisonaiagents/__init__.py | head -80Repository: MervinPraison/PraisonAI Length of output: 4636 🏁 Script executed: wc -l src/praisonai-agents/praisonaiagents/__init__.pyRepository: MervinPraison/PraisonAI Length of output: 119 🏁 Script executed: sed -n '530,580p' src/praisonai-agents/praisonaiagents/__init__.pyRepository: MervinPraison/PraisonAI Length of output: 1905 🏁 Script executed: sed -n '500,570p' src/praisonai-agents/praisonaiagents/__init__.py | cat -nRepository: MervinPraison/PraisonAI Length of output: 3155 🏁 Script executed: sed -n '570,620p' src/praisonai-agents/praisonaiagents/__init__.pyRepository: MervinPraison/PraisonAI Length of output: 1768 🏁 Script executed: sed -n '580,650p' src/praisonai-agents/praisonaiagents/__init__.py | cat -nRepository: MervinPraison/PraisonAI Length of output: 3065 🏁 Script executed: rg -A 10 "def _get_embedding_func" src/praisonai-agents/praisonaiagents/__init__.pyRepository: MervinPraison/PraisonAI Length of output: 487 Apply sys.modules restoration to prevent embedding proxy from being overwritten by submodule import. When Restore the proxy after import to maintain the intended lazy-load behavior: def _get_embedding_func():
"""Lazy getter for embedding function."""
from .embedding.embed import embedding as _embedding_func
# Restore proxy after import to prevent submodule from overwriting it
sys.modules[__name__].embedding = embedding
sys.modules[__name__].embeddings = embeddings
return _embedding_func🤖 Prompt for AI Agents |
||||||
|
|
||||||
| # Create lazy properties that override the submodule | ||||||
| class _EmbeddingProxy: | ||||||
| """Proxy object that loads embedding function on first access.""" | ||||||
| def __init__(self): | ||||||
| self._func = None | ||||||
|
|
||||||
| def _load(self): | ||||||
| """Load the actual embedding function if not already loaded.""" | ||||||
| if self._func is None: | ||||||
| self._func = _get_embedding_func() | ||||||
| return self._func | ||||||
|
|
||||||
| def __call__(self, *args, **kwargs): | ||||||
| return self._load()(*args, **kwargs) | ||||||
|
|
||||||
| def __getattr__(self, name): | ||||||
| return getattr(self._load(), name) | ||||||
|
|
||||||
| @property | ||||||
| def __wrapped__(self): | ||||||
| """Support for inspect.signature() and functools.wraps.""" | ||||||
| return self._load() | ||||||
|
|
||||||
| @property | ||||||
| def __signature__(self): | ||||||
| """Support for inspect.signature().""" | ||||||
| import inspect | ||||||
| return inspect.signature(self._load()) | ||||||
|
|
||||||
| def __repr__(self): | ||||||
| return f"<lazy proxy for {self._load()!r}>" | ||||||
|
|
||||||
| # Also provide embeddings alias | ||||||
| embeddings = _embedding_func | ||||||
| # Override the submodule with our function proxy | ||||||
| embedding = _EmbeddingProxy() | ||||||
| embeddings = embedding # embeddings is an alias | ||||||
|
Comment on lines
+543
to
+583
|
||||||
|
|
||||||
|
|
||||||
| # Create the __getattr__ function using centralized utility | ||||||
| __getattr__ = create_lazy_getattr_with_fallback( | ||||||
| mapping=_LAZY_IMPORTS, | ||||||
| module_name=__name__, | ||||||
| cache=_lazy_cache, | ||||||
| fallback_modules=['tools', 'memory', 'config', 'workflows'], | ||||||
| fallback_modules=[], # Note: 'embedding' excluded to avoid conflict with embedding() function | ||||||
| custom_handler=_custom_handler | ||||||
| ) | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
toolsis still listed in__all__/__dir__, but the eagerfrom . import toolswas removed and__getattr__currently doesn’t return the.toolssubmodule whenpa.toolsis accessed (fallback checks attributes inside submodules, not the submodule itself). This makesimport praisonaiagents as pa; pa.toolsraiseAttributeError. Consider adding an explicit handler (likememory/workflows) forname == 'tools'to returnimportlib.import_module('.tools', 'praisonaiagents'), or re-introduce a lightweight submodule import if acceptable.