Lazy Imports & Fast Startup

Optimize import time and memory usage with lazy loading

Lazy Imports & Fast Startup

PraisonAI Agents v0.5.0+ uses lazy imports to dramatically reduce startup time and memory usage. Heavy dependencies like litellm, requests, and chromadb are only loaded when actually needed.

python
from praisonaiagents import Agent

agent = Agent(name="MyAgent")  # litellm loads on first LLM call
agent.start("Hello")

The user imports PraisonAI and starts an agent instantly; heavy libraries load only on first use.

Diagram

🤖 Agent

⚡ Lazy Import

🚀 Fast Startup

How It Works

Diagram
LazyImportsFastAgentUserLazyImportsFastAgentUserRequestProcessResultResponse

Quick Start

  1. Import without heavy deps
    python
    from praisonaiagents import Agent
    
    agent = Agent(name="MyAgent")  # litellm loads on first LLM call
  2. Verify lazy loading
    python
    import sys
    import praisonaiagents
    
    assert "litellm" not in sys.modules
    print("Heavy dependencies not loaded at import time")

Performance Benefits

MetricBeforeAfterImprovement
Import Time820ms18ms97.8% faster
Memory Usage93.3MB33.0MB64.6% reduction

How It Works

Diagram
Lazy Imports & Fast StartupAgentUserLazy Imports & Fast StartupAgentUserRequestProcess requestResultResponse

Lazy Module Loading

Core modules are loaded on-demand using Python’s __getattr__ mechanism:

python
# These imports are fast - modules loaded lazily
from praisonaiagents import Agent, Session, Memory, Knowledge

# Agent is only fully loaded when you use it
agent = Agent(name="MyAgent")  # litellm loaded here

Heavy Dependencies

The following dependencies are NOT loaded at import time:

  • litellm – Only loaded when LLM calls are made
  • requests – Only loaded when HTTP calls are needed
  • chromadb – Only loaded when vector stores are used
  • mem0 – Only loaded when memory features are used

Wrapper CLI Lazy Loading

The praisonai CLI and praisonai.run(...) used to eagerly import the framework adapter tree (praisonaiagents.frameworks.*) on every invocation, wasting the ~420 ms deferral that agent.py already had. As of PR #4821 the agents_generator module is fully lazy — framework adapters load only when a framework is actually selected.

  • Runtime access goes through the lazy _get_default_adapter_registry(); framework-adapter imports live under TYPE_CHECKING, so importing praisonai.agents_generator never loads the framework tree.
python
import sys
import praisonai.agents_generator  # noqa: F401

assert not any(m.startswith("praisonaiagents.frameworks") for m in sys.modules), (
    "agents_generator must not eagerly load the framework tree"
)

As of PR #5892 the wrapper bootstrap (praisonai._bootstrap) also defers its pathlib import. _bootstrap runs on every import praisonai, but Path is only needed for the monorepo-dev sibling fallback, so the import now lives inside _ensure_sibling, past the find_spec early return. This saves roughly 26 ms on the installed path (pathlib plus urllib.parse, ipaddress, re, fnmatch) with no change in behaviour. The actual saving depends on whether another library you import already loads pathlib.

bash
# pathlib should no longer appear under praisonai._bootstrap
python -X importtime -c "import praisonai" 2>&1 | grep -E "pathlib|_bootstrap"

Training & Vision Module Lazy Loading

Modules affected: praisonai.train.llm.trainer (the TrainModel class) and praisonai.upload_vision (the UploadVisionModel class) now use lazy loading to defer heavy ML dependencies.

These modules use a _lazy_import_*_deps() helper called from __init__, mirroring train.py / train_vision.py patterns.

Dependencies deferred:

  • torch – CUDA/GPU computation framework
  • transformers (TextStreamer, TrainingArguments) – Hugging Face transformers
  • unsloth (FastLanguageModel, FastVisionModel, is_bfloat16_supported, standardize_sharegpt, get_chat_template) – Fast training optimization
  • trl (SFTTrainer) – Transformer Reinforcement Learning
  • datasets (load_dataset, concatenate_datasets) – Dataset loading utilities
  • psutil (virtual_memory) – System memory monitoring

Impact: Importing praisonai.upload_vision or praisonai.train.llm.trainer is now near-instant; CUDA / ~2 GB of ML libs only load when you instantiate UploadVisionModel(...) or TrainModel(...).

python
# Fast — no torch/unsloth load
from praisonai.upload_vision import UploadVisionModel

# Heavy deps load here, not at import time
uploader = UploadVisionModel(config_path="config.yaml")

ImportError messages now include install hints:

  • Vision upload: pip install torch unsloth
  • Training: pip install torch transformers unsloth datasets trl psutil

Opt-in-only Agent-path imports

As of PR #6170 (fixes #6167), three opt-in-only modules that previously loaded on every from praisonaiagents import Agent are now deferred to their first use. The public API is unchanged — only the import graph is tidier.

ModuleLoaded only whenOpt-in surface
praisonaiagents.agent.server_registryYou start the HTTP serverAgent.launch(...), PraisonAIAgents.launch(...)
praisonaiagents.goal.judgeThe goal gate runs for the first timeagent.run_goal(...), agent.run_goal_async(...), agent.run_until(..., goal=...)
praisonaiagents.session.encrypted_storeYou import or construct EncryptedSessionStorefrom praisonaiagents.session import EncryptedSessionStore

Verify none of them is in sys.modules after a bare Agent import:

python
import sys
from praisonaiagents import Agent  # noqa: F401

for mod in (
    "praisonaiagents.agent.server_registry",
    "praisonaiagents.goal.judge",
    "praisonaiagents.session.encrypted_store",
):
    assert mod not in sys.modules, mod

Each still works exactly as before — the module loads the first time you reach into the feature:

python
from praisonaiagents import Agent

agent = Agent(instructions="You are a helpful assistant")

# Goal loop — judge loads on the first gate evaluation
agent.run_goal("Tidy the repo and open a PR", goal="PR exists", max_turns=10)

# Encrypted session — encrypted_store loads on this import
from praisonaiagents.session import EncryptedSessionStore

# HTTP launch — server_registry loads on this call, not at import (blocks while serving)
agent.launch(path="/ask", port=8000)

Verifying Lazy Imports

You can verify lazy imports are working:

python
import sys

# Import the package
import praisonaiagents

# Check that heavy deps are NOT loaded
assert 'litellm' not in sys.modules
assert 'requests' not in sys.modules
assert 'chromadb' not in sys.modules

# Check training/vision modules are lazy loaded
from praisonai.upload_vision import UploadVisionModel  # noqa
assert "torch" not in sys.modules
assert "unsloth" not in sys.modules

print("✓ All heavy dependencies are lazy loaded")

Configuration

Lazy imports are enabled by default. You can check the configuration:

python
from praisonaiagents._config import LAZY_IMPORTS

print(f"Lazy imports enabled: {LAZY_IMPORTS}")

Shared lazy cache for optional-dep loading

praisonai._lazy_cache is the single source of truth for thread-safe optional-dependency loading — lazy_get(key, factory) returns a memoised value (including memoised exceptions), and lazy_reset(key) clears it.

python
# praisonaiagents-side (illustrative of the pattern PraisonAI uses internally)
from praisonai._lazy_cache import lazy_get, lazy_reset

def _get_workflow_models():
    def _create():
        from pydantic import BaseModel
        # ... class definitions ...
        return {"WorkflowStructure": WorkflowStructure, ...}
    return lazy_get("workflow_models", _create)

As of PR #4861, all three lazy-model factories in praisonai/auto.py (team, workflow, job workflow) route through the shared lazy_get, so exception caching and lazy_reset behave identically across them — no more ad-hoc _models_cache with divergent semantics. A broken pydantic install now fails fast and identically for every factory, and lazy_reset clears all three caches at once.

Transient vs structural failures

LazyCache(retry_cooldown: float = 30.0) splits cached loader failures by type, so a one-off environment blip no longer poisons the cache for the whole process lifetime.

Failure typeExamplesCache lifetime
StructuralImportError, ValueError, AttributeError, RuntimeErrorProcess lifetime — fails fast on every subsequent call
TransientOSError, ConnectionError, TimeoutErrorretry_cooldown seconds (default 30s), then the loader is retried
Control-flowKeyboardInterrupt, SystemExitNever cached — propagate immediately

A transient failure is timestamped when cached. Inside the cool-down window get(key, loader) re-raises the cached error; once the cool-down expires the next get retries the loader, and a success clears the cached failure. reset(key) clears both the value cache and the failure timestamp; reset() with no key clears everything.

Diagram

miss

structural error

transient error

no

yes

ok

fails

📥 get key

🔍 cached?

🔄 run loader

❌ raise

⏱️ cool-down
expired?

💾 store value

🕒 store timestamped failure

Use a scoped instance for multi-tenant callers instead of sharing the module-level singleton.

python
from praisonai._lazy_cache import LazyCache

# Scoped cache with a shorter cool-down for a tenant that expects
# transient errors to clear quickly.
cache = LazyCache(retry_cooldown=5.0)

The cool-down mirrors the one PraisonAIDB._init_stores already uses for the same failure mode. It closes a real failure: a long-lived process (praisonai serve) that hit a transient OSError on first litellm load previously returned the cached error for the entire process lifetime — every subsequent request in every tenant. With the cool-down, the process recovers automatically once the underlying condition clears. See PR #4948 (fixes #4945).

Best Practices

Import at point of use

Defer optional modules until a feature is invoked — keeps CLI and server startup fast.

Install extras only when needed

Use pip install praisonaiagents[memory] rather than pulling every optional dependency upfront.

Measure import time in CI

Track cold import duration to catch accidental module-level heavy imports in PRs.

Prefer lite for embedded use

Use the lite package when you bring your own LLM client and need minimal footprint.

Use a scoped LazyCache in multi-tenant callers

Construct a scoped LazyCache instance per tenant so a reset in one tenant does not stomp another, and tune retry_cooldown to how quickly transient failures are expected to clear.

python
# Good - specific imports
from praisonaiagents import Agent, Task

# Avoid - loads all modules
from praisonaiagents import *

Measuring Performance

Use the built-in benchmarks to measure import time:

python
import time

start = time.perf_counter()
import praisonaiagents
end = time.perf_counter()

print(f"Import time: {(end - start) * 1000:.1f}ms")

Related