PraisonAI Package Integration

Guide for integrating the PraisonAI package into your Python projects, including YAML configuration, execution modes, and the C8 wrapper–code boundary architecture

Include praisonai package in your project

Option 0: One-liner (simplest)

python
import praisonai

result = praisonai.run("agents.yaml")
print(result)

That’s it. praisonai.run() reuses the same framework auto-detection and LLM endpoint resolution the praisonai CLI uses, so anything that works on the command line works here.

What it does

  1. Step

    Auto-detects the first installed framework in this order: crewai → praisonaiagents → autogen. Pass framework="..." to override — ag2 and langgraph must be requested explicitly.

  2. Step

    Reads OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL_NAME (and the standard PraisonAI key/config files) — same as the CLI.

  3. Step

    Returns the final task output as a string.

Diagram

👤 User

📞 praisonai.run

🔍 Auto-detect framework

🔑 Resolve LLM endpoint

🤖 AgentsGenerator

✅ Result string

Async variant for FastAPI / Jupyter

python
import praisonai

async def handler():
    result = await praisonai.arun("agents.yaml")
    return result

arun() offloads the synchronous work to a thread so it never blocks the event loop.

Optional parameters

ParameterTypeDefaultDescription
agent_filestrrequiredPath to your agents.yaml.
framework`str \None`None (auto-detect)One of "crewai", "praisonai", "autogen", "ag2", "langgraph". ag2 and langgraph must be selected explicitly — they are not in the auto-detect chain. ag2 is registered but its adapter is an unimplemented stub; passing framework="ag2" will raise NotImplementedError until implemented.
tools`list \None`NoneMixed list of classes, plain functions and framework tool instances — see Passing tools.
agent_yaml`str \None`NoneInline YAML string; takes precedence over agent_file when both are given.
cli_config`dict \None`NoneAdvanced: override the resolved CLI config (rarely needed).

Passing tools

tools accepts a mixed list — PraisonAI registers each entry under the right name and skips anything it doesn’t recognise.

python
from praisonai import PraisonAI
from crewai_tools import SerperDevTool          # framework tool instance

def my_search(query: str) -> str:                # plain function
    return f"local search: {query}"

class MyScraper:                                 # class — instantiated for you
    def run(self, url: str) -> str:
        return f"scraped {url}"

praison = PraisonAI(
    agent_file="agents.yaml",
    tools=[my_search, SerperDevTool(), MyScraper],
)
praison.run()
Entry shapeHow it’s registeredExample
ClassInstantiated: tools_dict[MyTool.__name__] = MyTool()tools=[MyTool]
Plain callable (def or @tool function)Passed through by __name__tools=[my_search]
Framework tool instance (CrewAI / LangChain BaseTool)Passed through by .name (fallback: __name__, then type name)tools=[JiraSearchTool()]
Anything else (e.g. 42, a bare dict)Logged as a warning and skipped — the LLM never sees it—

Advanced CLI options from Python

run() and arun() accept any extra keyword argument the CLI accepts. Two loose names get a friendly Python alias; everything else is forwarded through the same cli_config bridge the CLI already uses.

python
import praisonai

# Same as: praisonai run agents.yaml --llm gpt-4o
result = praisonai.run("agents.yaml", model="gpt-4o")

# Same as: praisonai run agents.yaml --resume abc123
result = praisonai.run("agents.yaml", session="abc123")

# Any other CLI option — forwarded through cli_config
result = praisonai.run("agents.yaml", approval="plan", verbose=True)
Loose kwargMaps toNotes
model=config_list[0]["model"] (like --llm)Also accepts the alias llm=
session=resume_session (like --resume)Renamed from the CLI-side --session for Python readability
Any other kwargcli_config[key]Explicit cli_config values still win over loose kwargs
Diagram
graph LR
    Run[📞 praisonai.run] --> Kwargs{Loose kwarg?}
    Kwargs -->|model= / llm=| Model[Set config_list model]
    Kwargs -->|session=| Resume[Map to resume_session]
    Kwargs -->|anything else| Bridge[Forward via cli_config]
    Model --> Resolve[🔑 Resolve LLM endpoint]
    Resume --> Resolve
    Bridge --> Gen[🤖 AgentsGenerator]
    Resolve --> Gen

    classDef call fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef config fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef out fill:#10B981,stroke:#7C90A0,color:#fff

    class Run call
    class Kwargs check
    class Model,Resume,Bridge,Resolve config
    class Gen out

When to use PraisonAI(...) instead

Reach for the full PraisonAI class (Option 1 below) when you need:

  • Streaming / approval-system integration
  • Custom backends or the gradio UI
  • The auto="..." natural-language-to-YAML mode
  • Repeated runs against the same generator without re-parsing YAML
Diagram

Yes, just once

Need streaming/
approvals/auto-YAML

Inside async handler

Need to run an
agents.yaml?

praisonai.run

PraisonAI class

praisonai.arun

Option 1: Using RAW YAML

python
from praisonai import PraisonAI

# Example agent_yaml content
agent_yaml = """
framework: "crewai"
topic: "Space Exploration"

agents:  # Canonical: use 'agents' instead of 'roles'
  astronomer:
    role: "Space Researcher"
    goal: "Discover new insights about {topic}"
    instructions:  # Canonical: use 'instructions' instead of 'backstory' "You are a curious and dedicated astronomer with a passion for unraveling the mysteries of the cosmos."
    tasks:
      investigate_exoplanets:
        description: "Research and compile information about exoplanets discovered in the last decade."
        expected_output: "A summarized report on exoplanet discoveries, including their size, potential habitability, and distance from Earth."
"""

# Create a PraisonAI instance with the agent_yaml content
praisonai = PraisonAI(agent_yaml=agent_yaml)

# Run PraisonAI
result = praisonai.run()

# Print the result
print(result)

Option 2: Using separate agents.yaml file

Note: Please create agents.yaml file before hand. If you only need to run an agents.yaml once and want zero ceremony, see Option 0: One-liner above.

python
from praisonai import PraisonAI

def basic(): # Basic Mode
    praisonai = PraisonAI(agent_file="agents.yaml")
    praisonai.run()

if __name__ == "__main__":
    basic()

Other options

python
from praisonai import PraisonAI

def basic(): # Basic Mode
    praisonai = PraisonAI(agent_file="agents.yaml")
    praisonai.run()

def advanced(): # Advanced Mode with options
    praisonai = PraisonAI(
        agent_file="agents.yaml",
        framework="autogen", # use AG2 framework (Formerly AutoGen)
    )
    praisonai.run()

def auto(): # Full Automatic Mode
    praisonai = PraisonAI(
        auto="Create a movie script about car in mars",
        framework="autogen" # use AG2 framework
    )
    print(praisonai.framework)
    praisonai.run()

if __name__ == "__main__":
    basic()
    advanced()
    auto()

Lifecycle / cleanup

Each AgentsGenerator lazily creates a bounded thread-pool to run synchronous tools under per-call timeouts. The pool is owned by the instance, not the process — concurrent sessions in a multi-tenant runtime never share workers.

When you’re done with a generator, release the pool with .close() or use it as a context manager:

python
from praisonai.agents_generator import AgentsGenerator

# Pattern A: context manager (recommended)
with AgentsGenerator(agent_file="agents.yaml", framework="crewai", config_list=[...]) as gen:
    gen.generate_crew_and_kickoff()
# pool is shut down on exit, even if generate_crew_and_kickoff raises

# Pattern B: explicit close()
gen = AgentsGenerator(agent_file="agents.yaml", framework="crewai", config_list=[...])
try:
    gen.generate_crew_and_kickoff()
finally:
    gen.close()

close() is idempotent — calling it twice is safe.

Cross-generator tool isolation

Multiple AgentsGenerator instances can wrap the same shared framework tool object (common with CrewAI / LangChain tools registered once and reused across tenants) without interfering with each other. Each generator installs its own per-instance _TimeoutBoundTool proxy — a lightweight wrapper that delegates schema attributes (name, description, args_schema, …) to the shared inner tool but routes _run / run through its own executor. The shared inner object is never mutated in place, so shutting down one generator’s pool cannot break another generator’s calls on the same tool.

Diagram

delegates schema

delegates schema

_run / run

_run / run

Shared framework tool

Generator A
_TimeoutBoundTool proxy

Generator B
_TimeoutBoundTool proxy

Executor A

Executor B

The proxy is built as an instance of a dynamic subclass of the wrapped tool’s class, so isinstance(proxy, BaseTool) (and any framework-specific base class check) keeps returning True. This preserves the isinstance-based dispatch that praisonaiagents.tool_execution, CrewAI, and LangChain executors rely on to call .run — without it, a BaseTool plugin tool with tool_timeout set would silently execute nothing.

Injecting a shared executor (multi-tenant)

If you already manage a thread-pool (e.g. one per tenant), inject it via tool_timeout_executor=. The generator will use the pool but never shut it down — ownership stays with you.

python
import concurrent.futures
from praisonai.agents_generator import AgentsGenerator

tenant_pool = concurrent.futures.ThreadPoolExecutor(
    max_workers=8,
    thread_name_prefix=f"tenant-{tenant_id}",
)

with AgentsGenerator(
    agent_file="agents.yaml",
    framework="crewai",
    config_list=[...],
    tool_timeout_executor=tenant_pool,   # borrowed; close() will NOT shut it down
) as gen:
    gen.generate_crew_and_kickoff()

# you remain responsible for tenant_pool.shutdown()

Each owned pool is named praisonai-tool-timeout-<hexid> for easy enumeration in top / py-spy / thread dumps.

tool_timeout_executor parameter

ParameterTypeDefaultDescription
tool_timeout_executor`concurrent.futures.Executor \None`None (lazy, instance-owned)Optional executor for per-call tool timeouts. When None, the generator lazily creates and owns a bounded ThreadPoolExecutor (default 32 workers, override with PRAISONAI_TOOL_TIMEOUT_WORKERS). When supplied, it is borrowed — the generator never shuts it down.
strict_validation`bool \None`None (falls back to PRAISONAI_VALIDATE_STRICT)Per-run override that promotes YAML validation warnings to ValueError. Precedence: this arg > cli_config["strict_validation"] > PRAISONAI_VALIDATE_STRICT env var. Lets multi-tenant hosts scope strictness per request without mutating os.environ.
tool_timeout_workers`int \None`None (falls back to PRAISONAI_TOOL_TIMEOUT_WORKERS, default 32)Per-run override for the sync-tool timeout thread pool size. Precedence: this arg > cli_config["tool_timeout_workers"] > PRAISONAI_TOOL_TIMEOUT_WORKERS env var. Invalid values fall back to the env/default with a warning.

strict_validation and tool_timeout_workers are also recognised keys inside cli_config, so run() / arun() forward them through the same **kwargs bridge.

Always close or use `with`

Leaked tool-timeout pools accumulate threads named praisonai-tool-timeout-<hexid> across instances. Use with AgentsGenerator(...) as gen: or call gen.close() to release them. Check top / py-spy if you suspect thread accumulation.

Borrowed pools stay yours

Passing tool_timeout_executor= means close() won’t shut it down. You manage the lifecycle — call your_pool.shutdown() when you’re done with it.

One generator per session in multi-tenant

Don’t share AgentsGenerator instances across tenants. Share the executor instead — inject the same pool via tool_timeout_executor= into each per-tenant generator.

Sharing an AgentsGenerator across tenants is still discouraged (per-instance timeout executor lifecycles are per-instance), but sharing the underlying tool objects across generators is now safe — the per-generator proxy guarantees that A’s timeout wrapper cannot leak into B’s calls, and closing A’s pool cannot break B’s calls on the same tool.

Unique task names per workflow

With the CrewAI framework, context=[...] lookup keys on the task name. Two roles defining the same task name now raise ValueError — pick names like <role>_<verb> for clarity.


Logging in scripts

If you want PraisonAI to configure your application’s logging, call configure_cli_logging() once at startup:

python
from praisonai import PraisonAI
from praisonai._logging import configure_cli_logging

configure_cli_logging("INFO")  # opt in to root-logger setup
PraisonAI(agent_file="agents.yaml").run()

If you want PraisonAI to leave your application’s logging untouched, just don’t call configure_cli_logging — only namespaced praisonai.* loggers will be used.


C9 Architecture: Four-Tier Package Model (Developer Reference)

Sibling: praisonai-bot

As of C9 (merged 2026-07-03), bots, gateway, and channel CLI live in a new sibling PyPI package praisonai-bot (module praisonai_bot). The praisonai.bots, praisonai.gateway, and praisonai.daemon paths remain as alias_package shims and are the stable public API.

Four-Tier Model

PraisonAI uses a strict four-tier package model with a one-way dependency rule:

Diagram

Tier 3 — praisonai (Wrapper)

Tier 2 — praisonai-bot (Bots / Gateway)

Tier 2 — praisonai-code (Terminal CLI)

Tier 1 — praisonaiagents (Core SDK)

lazy bridge only — no PyPI dep

lazy bridge only — no PyPI dep

Protocols / Hooks / Base Classes

cli/protocols.py

run / chat / code / serve

_wrapper_bridge.py

bots / gateway / daemon / claw

_wrapper_bridge.py

Repatriated Commands

Repatriated Features

adapters/

alias_package shims for bots/gateway/daemon

Invariant: Both praisonai-code and praisonai-bot declare no PyPI dependency on praisonai. All cross-tier access goes through _wrapper_bridge only.

alias_package shim: lazy vs eager

The praisonai.bots / praisonai.gateway / praisonai.daemon alias_package shims default to an eager subtree walk at shim-install time. This preserves old_name.sub is new_name.sub — the module-identity guarantee that keeps unittest.mock.patch("old.sub.attr") and isinstance checks working after the C9 migration, even when the running code imports the moved package under its new name.

Set PRAISONAI_SHIM_LAZY=1 to opt out of the eager walk (pure _AliasFinder lazy resolution). The trade-off: faster shim install and no eager pull of optional heavy deps — but mock.patch against the old dotted path can miss when code imports via the new name.

As of PR #4261, a failed eager import is no longer silently swallowed — it logs a warning with traceback (shim: eager import of %s failed; will be resolved lazily) and is left for _AliasFinder to resolve lazily. None placeholders in sys.modules are skipped instead of poisoning the old dotted alias. The warning is a useful debugging aid when a downstream AttributeError on the alias would otherwise have no traceback pointing back at the shim.

When to keep the default (eager)

Keep eager registration whenever anything in the process relies on mock.patch against the old dotted path (praisonai.bots., praisonai.gateway., praisonai.daemon.*) or on old_name.sub is new_name.sub identity.

When to set PRAISONAI_SHIM_LAZY=1

Use it only when no mock.patch targets the old dotted path and minimal-container startup speed matters more than mock-patch parity. See Security Environment Variables → PRAISONAI_SHIM_LAZY.

C8 Architecture: Wrapper–Code Boundary (Historical Reference)

Three-Tier Model (pre-C9 / C8)

Before C9, PraisonAI used a three-tier package model:

Diagram

Tier 3 — praisonai (Wrapper)

Tier 2 — praisonai-code (Terminal CLI)

Tier 1 — praisonaiagents (Core SDK)

lazy bridge only — no PyPI dep

Protocols / Hooks / Base Classes

cli/protocols.py

run / chat / code / serve

_wrapper_bridge.py

Repatriated Commands

Repatriated Features

adapters/

Invariant: praisonai-code/pyproject.toml declares no PyPI dependency on praisonai. All praisonai-code → praisonai wrapper access goes through praisonai_code._wrapper_bridge only.

C8 Metrics (post-C8, merged 2026-07-02)

MetricPre-C8Post-C8
Wrapper import lines in praisonai-code2250
Regression baseline in check_c7_imports.sh22550
Allowlisted files470

_wrapper_bridge — The Only Cross-Tier Path

praisonai_code._wrapper_bridge is the sole permitted mechanism for praisonai-code to call into praisonai. It lazy-loads the wrapper at runtime and never causes an import-time error when the wrapper is absent.

python
from praisonai_code._wrapper_bridge import get_wrapper_module

mod = get_wrapper_module("praisonai.cli.commands.train")
if mod:
    mod.train_command()

Direct import praisonai or from praisonai import … inside praisonai-code is forbidden and enforced by scripts/check_c7_imports.sh.

_WRAPPER_RESIDENT_COMMANDS

Commands that live in the praisonai wrapper but are registered in the praisonai-code CLI router. The variable was renamed from _WRAPPER_COMMANDS in C8.1 (backward-compat alias retained).

get_command() lazy-loads these via the absolute praisonai.cli.commands.* path through the bridge.

C8.2 Repatriated Commands (moved from praisonai-code back to praisonai):

BatchCommands
Alangfuse, langextract, flow, n8n, replay, langfuse_client
Btrain, managed, examples, standardise
Cdocs, schedule, batch
Drag, knowledge, realtime, profile, audit, app
Eserve wrapper paths (SDK subcommands remain in praisonai-code)

C8.3 Repatriated Features

The following cli/features/* modules moved from praisonai-code to praisonai/cli/features/:

recipe, templates, deploy, recipe_optimizer, persistence, eval, agent_scheduler, acp, registry, sandbox_cli, ollama, workflow, tui/app, interactive/async_tui, interactive/core

Also repatriated: commands/recipe, context, mcp, validate.

Protocols & Adapters (C8.5)

C8.5 introduces typed protocol contracts to enforce the tier boundary. The SessionStoreProtocol (session persistence) already ships in praisonaiagents:

python
from praisonaiagents.session.protocols import SessionStoreProtocol

TemplateStoreProtocol and ServeHandlerProtocol are defined in the C8.5 design but their physical extraction is deferred (same milestone as the PraisonAI class split).

ProtocolLocationPurpose
SessionStoreProtocolpraisonaiagents.session.protocolsSession persistence backend contract (add_message, get_chat_history, …)
TemplateStoreProtocol_deferred_Template persistence for repatriated CLI features
ServeHandlerProtocol_deferred_Dispatch serve subcommands without direct wrapper imports

The praisonai.adapters module lazily re-exports existing adapter classes (AutoReader, ChromaVectorStore, BasicRetriever, FusionRetriever, LLMReranker, and registration helpers) to preserve backward compatibility. Registration into the core-SDK singletons is now automatic at the wrapper entry points (praisonai.run, praisonai.arun, and every praisonai CLI command that runs an agent file) — register_default_adapters() is called for you before the agent starts, so YAML declarations like retriever: fusion and reranker: llm resolve out of the box (PraisonAI 4.7.9+). Library-mode Python callers importing praisonai.adapters.* directly still call register_default_adapters() themselves (idempotent + lock-guarded, so a subsequent auto-call is a no-op). Multi-tenant hosts can pre-register custom adapters under built-in names — the wrapper defaults preserve any existing entry (register-only-if-absent) and never overwrite.

Writing a custom managed backend

ManagedBackendBase provides the shared async stream() producer-thread + sentinel-queue pump. AnthropicManagedAgent and LocalManagedAgent both inherit from it; a third managed backend (E2B, Modal, Fly.io, …) implements two hooks instead of copying the queue loop.

MethodTypeRequired?Purpose
_ensure_session(self) -> strsync✅Return a session id; called on the producer thread before _iter_events
_iter_events(self, session_id, prompt) -> Iterator[str]sync generator✅Yield text chunks for prompt on session_id; runs on the producer thread so blocking SDK calls are fine
_after_stream(self, full: str) -> Nonesync⬜Post-stream bookkeeping (persist the assembled assistant turn, sync usage). Default: no-op

_ensure_session runs before the first _iter_events call. A backend that snapshots session state should populate agent_id / environment_id inside _ensure_session — LocalManagedAgent._ensure_session() creates the inner agent first for exactly this reason, so the first persisted snapshot is complete rather than orphaned.

python
from praisonai.integrations._managed_base import ManagedBackendBase

class MyManagedAgent(ManagedBackendBase):
    def _ensure_session(self) -> str:
        # Create/lookup your session with your provider; return its id.
        return self._session_id or self._provision_session()

    def _iter_events(self, session_id, prompt):
        # Runs on the producer thread — blocking SDK calls are fine here.
        for chunk in self._provider_client.stream(session_id, prompt):
            if chunk.text:
                yield chunk.text

    def _after_stream(self, full: str) -> None:
        # Optional: persist the assembled assistant turn + usage.
        self._persist_message("assistant", full)

C8.4 Legacy Structure

  • praisonai/cli/legacy/inbuilt_tools.py, framework_run.py — extraction targets for lazy loaders.
  • praisonai-code/cli/legacy/prompt_dispatch.py — standalone-safe helpers.
  • main.py still contains the PraisonAI class body; wrapper access is normalised via the bridge. The physical 7k-line split is deferred (out of scope of C8).

Developer Tooling (C8)

ScriptPurpose
scripts/check_c7_imports.shCounts wrapper import lines in praisonai-code; enforces baseline ≤ 50
scripts/c8_bridge_convert.pyConverts direct from praisonai … imports to _wrapper_bridge calls
scripts/c8_repatriate.pyMoves a command/feature implementation from praisonai-code → praisonai

Deferred Work (C8)

The physical extraction of the PraisonAI class (~7k lines in main.py) to praisonai/cli/legacy/praison_class.py was explicitly deferred from C8. The docs/concepts/architecture.mdx page (HUMAN-ONLY) also needs a maintainer update to reflect the C8 metrics.