Skip to content

Commit 9b85a22

Browse files
fix: implement lazy loading for tools imports to improve package startup time (#1172)
* fix: implement lazy loading for tools imports to improve package startup time - Remove eager imports of tools modules from __init__.py (lines 63-66, 72) - Add tools imports to _LAZY_IMPORTS mapping for proper lazy loading - Create embedding proxy to resolve function vs subpackage conflict - Maintain 100% backward compatibility with existing imports - Improve startup performance by loading tools only when accessed Fixes #1168 This addresses the core SDK eagerly importing heavy modules issue by: 1. Moving from .tools.* imports to lazy loading system 2. Using existing _LAZY_IMPORTS infrastructure 3. Resolving embedding function import conflict via proxy 4. Zero breaking changes to public API Performance improvement: Base import reduced from heavy module loading to lightweight protocol-driven imports following AGENTS.md principles. Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com> * fix: address lazy loading implementation issues - Remove eager config import, move to lazy loading in _custom_handler - Remove heavy modules from fallback_modules to prevent imports on typos - Add proper tools/config/memory/workflows module access via _custom_handler - Fix _EmbeddingProxy introspection with __wrapped__ and __signature__ properties - Remove dead code in _custom_handler for embedding/embeddings - Clean up duplicate comments and self-referential error messages - Maintain full backward compatibility while improving startup performance Fixes all issues identified in code review by Copilot, CodeRabbit, and Qodo. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com> * fix: address lazy loading implementation issues - Fix _get_embedding_func to avoid overwriting embedding proxy - Clean up dead code comments - Update comments for clarity - All validation tests pass: pa.tools, pa.config, embedding introspection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Mervin Praison <MervinPraison@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
1 parent 9a79f2a commit 9b85a22

1 file changed

Lines changed: 74 additions & 31 deletions

File tree

src/praisonai-agents/praisonaiagents/__init__.py

Lines changed: 74 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,7 @@
5959
# Import configuration (lightweight, no heavy deps)
6060
from . import _config
6161

62-
# Lightweight imports that don't trigger heavy dependency chains
63-
from .tools.tools import Tools
64-
from .tools.base import BaseTool, ToolResult, ToolValidationError, validate_tool
65-
from .tools.decorator import tool, FunctionTool
66-
from .tools.registry import get_registry, register_tool, get_tool, ToolRegistry
67-
# db and obs are lazy-loaded via __getattr__ for performance
68-
69-
# Sub-packages for organized imports (pa.config, pa.tools, etc.)
70-
# These enable: import praisonaiagents as pa; pa.config.MemoryConfig
71-
from . import config
72-
from . import tools
73-
# Note: db, obs, knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps
62+
# Note: tools, config, memory, workflows, db, obs, knowledge and mcp are lazy-loaded via __getattr__ due to heavy deps
7463

7564
# Embedding API - LAZY LOADED via __getattr__ for performance
7665
# Supports: embedding, embeddings, aembedding, aembeddings, EmbeddingResult, get_dimensions
@@ -125,6 +114,19 @@ def _get_lazy_cache():
125114
# ============================================================================
126115

127116
_LAZY_IMPORTS = {
117+
# Tools (moved from eager imports for lazy loading)
118+
'Tools': ('praisonaiagents.tools.tools', 'Tools'),
119+
'BaseTool': ('praisonaiagents.tools.base', 'BaseTool'),
120+
'ToolResult': ('praisonaiagents.tools.base', 'ToolResult'),
121+
'ToolValidationError': ('praisonaiagents.tools.base', 'ToolValidationError'),
122+
'validate_tool': ('praisonaiagents.tools.base', 'validate_tool'),
123+
'tool': ('praisonaiagents.tools.decorator', 'tool'),
124+
'FunctionTool': ('praisonaiagents.tools.decorator', 'FunctionTool'),
125+
'get_registry': ('praisonaiagents.tools.registry', 'get_registry'),
126+
'register_tool': ('praisonaiagents.tools.registry', 'register_tool'),
127+
'get_tool': ('praisonaiagents.tools.registry', 'get_tool'),
128+
'ToolRegistry': ('praisonaiagents.tools.registry', 'ToolRegistry'),
129+
128130
# Main display utilities (imports rich)
129131
'TaskOutput': ('praisonaiagents.main', 'TaskOutput'),
130132
'ReflectionOutput': ('praisonaiagents.main', 'ReflectionOutput'),
@@ -179,9 +181,7 @@ def _get_lazy_cache():
179181
'HandoffDepthError': ('praisonaiagents.agent.handoff', 'HandoffDepthError'),
180182
'HandoffTimeoutError': ('praisonaiagents.agent.handoff', 'HandoffTimeoutError'),
181183

182-
# Embedding API
183-
'embedding': ('praisonaiagents.embedding.embed', 'embedding'),
184-
'embeddings': ('praisonaiagents.embedding.embed', 'embedding'),
184+
# Embedding API (Note: embedding/embeddings handled in custom_handler to override subpackage)
185185
'aembedding': ('praisonaiagents.embedding.embed', 'aembedding'),
186186
'aembeddings': ('praisonaiagents.embedding.embed', 'aembedding'),
187187
'embed': ('praisonaiagents.embedding.embed', 'embed'),
@@ -506,21 +506,27 @@ def _custom_handler(name, cache):
506506
cache['Agents'] = value
507507
return value
508508

509-
# Task removed in v4.0.0 - use Task instead
510-
if name == "Task":
511-
raise ImportError(
512-
"Task has been removed in v4.0.0. Use Task instead.\n"
513-
"Migration: Replace 'from praisonaiagents import Task' with 'from praisonaiagents import Task'\n"
514-
"Task supports all Task features including action, handler, loop_over, etc."
515-
)
516-
517509
# Module imports (return the module itself)
510+
if name == 'tools':
511+
import importlib
512+
mod = importlib.import_module('.tools', 'praisonaiagents')
513+
cache['tools'] = mod
514+
return mod
515+
if name == 'config':
516+
import importlib
517+
mod = importlib.import_module('.config', 'praisonaiagents')
518+
cache['config'] = mod
519+
return mod
518520
if name == 'memory':
519521
import importlib
520-
return importlib.import_module('.memory', 'praisonaiagents')
522+
mod = importlib.import_module('.memory', 'praisonaiagents')
523+
cache['memory'] = mod
524+
return mod
521525
if name == 'workflows':
522526
import importlib
523-
return importlib.import_module('.workflows', 'praisonaiagents')
527+
mod = importlib.import_module('.workflows', 'praisonaiagents')
528+
cache['workflows'] = mod
529+
return mod
524530

525531
raise AttributeError(f"Not handled by custom_handler: {name}")
526532

@@ -532,20 +538,57 @@ def _custom_handler(name, cache):
532538
# Override them here to return the function instead of the module.
533539
# ============================================================================
534540

535-
# Override 'embedding' to return the function, not the subpackage
536-
from .embedding.embed import embedding as _embedding_func
537-
embedding = _embedding_func
541+
# Override 'embedding' and 'embeddings' at module level to prevent subpackage import
542+
# These need to be set after _LAZY_IMPORTS is defined but before __getattr__ is created
543+
def _get_embedding_func():
544+
"""Lazy getter for embedding function."""
545+
# Import with alias to avoid overwriting the module proxy
546+
from .embedding.embed import embedding as _embedding_func
547+
return _embedding_func
548+
549+
# Create lazy properties that override the submodule
550+
class _EmbeddingProxy:
551+
"""Proxy object that loads embedding function on first access."""
552+
def __init__(self):
553+
self._func = None
554+
555+
def _load(self):
556+
"""Load the actual embedding function if not already loaded."""
557+
if self._func is None:
558+
self._func = _get_embedding_func()
559+
return self._func
560+
561+
def __call__(self, *args, **kwargs):
562+
return self._load()(*args, **kwargs)
563+
564+
def __getattr__(self, name):
565+
return getattr(self._load(), name)
566+
567+
@property
568+
def __wrapped__(self):
569+
"""Support for inspect.signature() and functools.wraps."""
570+
return self._load()
571+
572+
@property
573+
def __signature__(self):
574+
"""Support for inspect.signature()."""
575+
import inspect
576+
return inspect.signature(self._load())
577+
578+
def __repr__(self):
579+
return f"<lazy proxy for {self._load()!r}>"
538580

539-
# Also provide embeddings alias
540-
embeddings = _embedding_func
581+
# Override the submodule with our function proxy
582+
embedding = _EmbeddingProxy()
583+
embeddings = embedding # embeddings is an alias
541584

542585

543586
# Create the __getattr__ function using centralized utility
544587
__getattr__ = create_lazy_getattr_with_fallback(
545588
mapping=_LAZY_IMPORTS,
546589
module_name=__name__,
547590
cache=_lazy_cache,
548-
fallback_modules=['tools', 'memory', 'config', 'workflows'],
591+
fallback_modules=[], # Note: 'embedding' excluded to avoid conflict with embedding() function
549592
custom_handler=_custom_handler
550593
)
551594

0 commit comments

Comments
 (0)