You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In-depth analysis of src/praisonai/praisonai/ against the project's stated philosophy (Protocol-driven Β· DRY Β· Multi-agent + async safe Β· Performance-first Β· Production-ready). The findings below are not stylistic β each one has either been observed to cause behavioural drift between code paths, silently lose data, or break a feature the YAML schema advertises.
This report focuses on the top 3 highest-impact gaps with code citations and concrete fixes.
1. agents_generator.py β sync and async paths are copy-pasted, and have already drifted into correctness bugs
AgentsGenerator.generate_crew_and_kickoff() (sync, lines 472β626) and AgentsGenerator._arun_framework() (async, lines 660β795) implement the same end-to-end pipeline: load YAML, apply CLI overrides, normalise agents β roles, validate config, collect tools from YAML, resolve them, instantiate them, pick a framework adapter, set up observability, dispatch to run/arun. They duplicate roughly 150 lines with the only real change being run(...) β await arun(...). The same problem exists between _run_yaml_workflow (851) and _arun_yaml_workflow (797).
This isn't theoretical β the two branches have already diverged in ways that change behaviour:
1a. AutoGen version selection only exists on the async path
agents_generator.py:744-763 (async path) reads autogen_version from YAML/env and chooses between autogen and autogen_v4. The sync path has no equivalent β it just uses whatever adapter was resolved earlier (line 599-600). A user who configures:
framework: autogenautogen_version: v0.4
and runs synchronously gets autogen v0.2 silently. Running the same file via the async API picks v0.4. Same YAML, different framework.
1b. _validate_cli_backend_compatibility only runs on the async path
There is no matching call in generate_crew_and_kickoff(). A YAML that combines cli_backend: with framework: crewai is rejected with a clear ValueError from arun(...), but silently accepted by the sync path and then fails deep inside the CrewAI adapter with a confusing error.
1c. Tool resolution + instantiation differ between paths
# Sync β agents_generator.py:558-566fortool_nameinneeded_tools:
try:
resolved_tool=self.tool_resolver.resolve(tool_name, instantiate=True)
ifresolved_toolisnotNone:
tools_dict[tool_name] =resolved_toolexceptExceptionase:
self.logger.warning(f"Failed to resolve or instantiate tool '{tool_name}': {e}")
continue
The semantics are almost the same, but: (a) the sync path goes through ToolResolver.resolve(..., instantiate=True) which returns the instantiated cached value, while the async path instantiates a fresh instance on every call (because it skips the instantiate flag and does it itself); (b) only the async path warns on unresolved tools; (c) the warning message is different ("Failed to resolve or instantiate" vs "Failed to initialize"), which breaks log-based alerting.
Suggested fix
Extract the shared pipeline into a helper that returns a prepared (adapter, config, llm_config, topic, tools_dict, ...) tuple, and have both entry points dispatch to adapter.run / adapter.arun from a single source of truth. Sketch:
# agents_generator.pydef_prepare_for_run(self, config: dict) ->"_PreparedRun":
"""Single source of truth for YAML normalisation, validation, CLI-backend compatibility, tool resolution, AutoGen version selection, and adapter resolution. Used by BOTH sync and async."""self._normalise_canonical_format(config)
self._validate_agents_config(config)
self._validate_cli_backend_compatibility(config, self.framework) # always runtopic=config.get('input', config.get('topic', ''))
tools_dict=self._build_tools_dict(config) # shared loopframework=self._select_autogen_version( # shared logicself.frameworkorconfig.get('framework', 'crewai'),
config,
)
adapter=self._get_framework_adapter(framework).resolve()
assert_framework_available(adapter.name)
adapter.setup(framework_tag=adapter.name)
return_PreparedRun(adapter, config, topic, tools_dict)
defgenerate_crew_and_kickoff(self):
config=self._load_config()
ifself._is_workflow(config):
returnself._run_yaml_workflow(config)
ifself.cli_config:
self._merge_cli_config(config, self.cli_config)
prep=self._prepare_for_run(config)
returnprep.adapter.run(
prep.config, self.config_list, prep.topic,
tools_dict=prep.tools_dict,
agent_callback=self.agent_callback,
task_callback=self.task_callback,
cli_config=self.cli_config,
)
asyncdefagenerate_crew_and_kickoff(self):
config=self._load_config()
ifself._is_workflow(config):
returnawaitself._arun_yaml_workflow(config)
ifself.cli_config:
self._merge_cli_config(config, self.cli_config)
prep=self._prepare_for_run(config)
returnawaitprep.adapter.arun(
prep.config, self.config_list, prep.topic,
tools_dict=prep.tools_dict,
agent_callback=self.agent_callback,
task_callback=self.task_callback,
cli_config=self.cli_config,
)
This eliminates the drift in a single change and aligns with the project's DRY pillar.
2. db/adapter.py β write hooks check self._conversation_storebefore_init_stores(), so any write that arrives before on_agent_start is silently dropped
PraisonAIDB.__init__ sets self._conversation_store = None and defers backend construction to _init_stores(), which is called lazily. Read-side hooks (on_agent_start, get_runs, export_session, import_session, ...) all call _init_stores() first β verified by grep _init_stores db/adapter.py, which finds it on lines 153, 337, 369, 404, 527, 569, 594, 617, 642, 664, 697, 706.
The write-side hooks do not:
# db/adapter.py:191-212defon_user_message(self, session_id, content, metadata=None) ->None:
"""Called when user sends a message."""ifnotself._conversation_store: # <-- still None until on_agent_start runsreturn
...
self._conversation_store.add_message(session_id, msg)
The same pattern is in on_agent_message (214), on_tool_call (237), on_agent_end (270), on_run_start (285), and on_run_end (309).
If any of those hooks fires beforeon_agent_start β which happens with REST endpoints that POST a message into a session that wasn't opened via on_agent_start, with imported sessions, or with hosted backends that route messages through the adapter directly β the data is silently lost. There is no exception, no warning, no telemetry.
Verification
frompraisonai.dbimportPraisonAIDBdb=PraisonAIDB(database_url="sqlite:///./test.db")
db.on_user_message("s1", "hello") # silently dropped: stores never initialiseddb.on_agent_start("agent", "s1") # NOW stores initialisedb.on_user_message("s1", "world") # this one is persisted
Suggested fix
Add self._init_stores() to the top of every write hook, mirroring the read hooks:
3. framework_adapters/crewai_adapter.py β string-form llm: YAML crashes CrewAI but works on PraisonAI, and the same 4-line block is duplicated 4 times
The YAML schema accepts two forms for the per-agent llm: field. The PraisonAI adapter handles both:
works under framework: praisonai and crashes under framework: crewai with AttributeError: 'str' object has no attribute 'get'. This violates the project's "3-way feature surface" β the YAML knob behaves differently depending on which adapter executes it.
The same PraisonAIModel(...).get_model() block is duplicated 4 times in crewai_adapter.py:81-104 (for llm and function_calling_llm, each with a "has spec / no spec" branch). The hard-coded "openai/gpt-4o-mini" and the base_url/api_key plumbing are also repeated in praisonai_adapter.py and autogen_adapter.py.
Suggested fix
Promote _resolve_agent_model (or a stronger variant returning a fully-built PraisonAIModel) onto BaseFrameworkAdapter so every adapter uses one resolver, and collapse the 4 branches in crewai_adapter.py:
# framework_adapters/base.pyclassBaseFrameworkAdapter:
DEFAULT_MODEL="openai/gpt-4o-mini"def_resolve_llm(self, spec, llm_config, *, field="llm"):
"""Build a PraisonAIModel from a per-agent llm/function_calling_llm spec. Accepts str, dict, or None. Single source of truth for all adapters."""from ..incimportPraisonAIModelbase=llm_config[0].get('base_url') ifllm_configelseNonekey=llm_config[0].get('api_key') ifllm_configelseNoneifisinstance(spec, str) andspec.strip():
model=spec.strip()
elifisinstance(spec, dict) andspec.get('model'):
model=spec['model']
else:
model=os.environ.get("MODEL_NAME") orself.DEFAULT_MODELreturnPraisonAIModel(model=model, base_url=base, api_key=key).get_model()
This fixes the string-form crash, removes the 4x duplication, makes MODEL_NAME / DEFAULT_MODEL resolution uniform across adapters, and gives PraisonAI/AutoGen adapters the same helper.
Validation
All file paths and line numbers are against current main (commit at session start).
The sync/async drift (Github actions fixΒ #1) was confirmed by reading both functions end-to-end and diffing them mentally β the only line that has to differ is run(...) vs await arun(...).
The DB silent-drop (Merge pull request #1 from MervinPraison/developΒ #2) was confirmed by grep _init_stores db/adapter.py showing the call appears on every read-side hook but is missing from on_user_message / on_agent_message / on_tool_call / on_agent_end / on_run_start / on_run_end.
The adapter inconsistency (MainΒ #3) was confirmed by reading both _resolve_agent_model in praisonai_adapter.py:26 and the bare .get("model") call in crewai_adapter.py:82.
Out of scope for this issue (per request): docs, tests, coverage, file-size, line-count concerns.
Scope
In-depth analysis of
src/praisonai/praisonai/against the project's stated philosophy (Protocol-driven Β· DRY Β· Multi-agent + async safe Β· Performance-first Β· Production-ready). The findings below are not stylistic β each one has either been observed to cause behavioural drift between code paths, silently lose data, or break a feature the YAML schema advertises.This report focuses on the top 3 highest-impact gaps with code citations and concrete fixes.
1.
agents_generator.pyβ sync and async paths are copy-pasted, and have already drifted into correctness bugsAgentsGenerator.generate_crew_and_kickoff()(sync, lines 472β626) andAgentsGenerator._arun_framework()(async, lines 660β795) implement the same end-to-end pipeline: load YAML, apply CLI overrides, normaliseagentsβroles, validate config, collect tools from YAML, resolve them, instantiate them, pick a framework adapter, set up observability, dispatch torun/arun. They duplicate roughly 150 lines with the only real change beingrun(...)βawait arun(...). The same problem exists between_run_yaml_workflow(851) and_arun_yaml_workflow(797).This isn't theoretical β the two branches have already diverged in ways that change behaviour:
1a. AutoGen version selection only exists on the async path
agents_generator.py:744-763(async path) readsautogen_versionfrom YAML/env and chooses betweenautogenandautogen_v4. The sync path has no equivalent β it just uses whatever adapter was resolved earlier (line 599-600). A user who configures:and runs synchronously gets
autogenv0.2 silently. Running the same file via the async API picks v0.4. Same YAML, different framework.1b.
_validate_cli_backend_compatibilityonly runs on the async pathThere is no matching call in
generate_crew_and_kickoff(). A YAML that combinescli_backend:withframework: crewaiis rejected with a clearValueErrorfromarun(...), but silently accepted by the sync path and then fails deep inside the CrewAI adapter with a confusing error.1c. Tool resolution + instantiation differ between paths
The semantics are almost the same, but: (a) the sync path goes through
ToolResolver.resolve(..., instantiate=True)which returns the instantiated cached value, while the async path instantiates a fresh instance on every call (because it skips theinstantiateflag and does it itself); (b) only the async path warns on unresolved tools; (c) the warning message is different ("Failed to resolve or instantiate" vs "Failed to initialize"), which breaks log-based alerting.Suggested fix
Extract the shared pipeline into a helper that returns a prepared
(adapter, config, llm_config, topic, tools_dict, ...)tuple, and have both entry points dispatch toadapter.run/adapter.arunfrom a single source of truth. Sketch:This eliminates the drift in a single change and aligns with the project's DRY pillar.
2.
db/adapter.pyβ write hooks checkself._conversation_storebefore_init_stores(), so any write that arrives beforeon_agent_startis silently droppedPraisonAIDB.__init__setsself._conversation_store = Noneand defers backend construction to_init_stores(), which is called lazily. Read-side hooks (on_agent_start,get_runs,export_session,import_session, ...) all call_init_stores()first β verified bygrep _init_stores db/adapter.py, which finds it on lines153, 337, 369, 404, 527, 569, 594, 617, 642, 664, 697, 706.The write-side hooks do not:
The same pattern is in
on_agent_message(214),on_tool_call(237),on_agent_end(270),on_run_start(285), andon_run_end(309).If any of those hooks fires before
on_agent_startβ which happens with REST endpoints that POST a message into a session that wasn't opened viaon_agent_start, with imported sessions, or with hosted backends that route messages through the adapter directly β the data is silently lost. There is no exception, no warning, no telemetry.Verification
Suggested fix
Add
self._init_stores()to the top of every write hook, mirroring the read hooks:A cleaner long-term fix is a decorator so this can't be forgotten on the next hook added:
3.
framework_adapters/crewai_adapter.pyβ string-formllm:YAML crashes CrewAI but works on PraisonAI, and the same 4-line block is duplicated 4 timesThe YAML schema accepts two forms for the per-agent
llm:field. The PraisonAI adapter handles both:The CrewAI adapter doesn't β it assumes dict:
A YAML like
works under
framework: praisonaiand crashes underframework: crewaiwithAttributeError: 'str' object has no attribute 'get'. This violates the project's "3-way feature surface" β the YAML knob behaves differently depending on which adapter executes it.The same
PraisonAIModel(...).get_model()block is duplicated 4 times increwai_adapter.py:81-104(forllmandfunction_calling_llm, each with a "has spec / no spec" branch). The hard-coded"openai/gpt-4o-mini"and thebase_url/api_keyplumbing are also repeated inpraisonai_adapter.pyandautogen_adapter.py.Suggested fix
Promote
_resolve_agent_model(or a stronger variant returning a fully-builtPraisonAIModel) ontoBaseFrameworkAdapterso every adapter uses one resolver, and collapse the 4 branches increwai_adapter.py:This fixes the string-form crash, removes the 4x duplication, makes
MODEL_NAME/DEFAULT_MODELresolution uniform across adapters, and gives PraisonAI/AutoGen adapters the same helper.Validation
main(commit at session start).run(...)vsawait arun(...).grep _init_stores db/adapter.pyshowing the call appears on every read-side hook but is missing fromon_user_message/on_agent_message/on_tool_call/on_agent_end/on_run_start/on_run_end._resolve_agent_modelinpraisonai_adapter.py:26and the bare.get("model")call increwai_adapter.py:82.Out of scope for this issue (per request): docs, tests, coverage, file-size, line-count concerns.