fix: resolve three high-impact gaps in praisonai wrapper layer#1736
fix: resolve three high-impact gaps in praisonai wrapper layer#1736praisonai-triage-agent[bot] wants to merge 4 commits into
Conversation
…1735) - Fix blocking ollama serve calls: Replace subprocess.run(['ollama', 'serve']) with proper daemon management across 4 files. Created shared _ollama.py helper with non-blocking daemon startup and connection checking. - Fix multi-tenant unsafe tool resolver: Resolve paths eagerly in constructor instead of lazy CWD binding. Add reset_default_resolver() function to clear process-level cache, following _framework_availability.invalidate() pattern. - Fix sync-only auto-generator: Add async API with _astructured_completion(), agenerate() methods, and proper async context managers (__aenter__/__aexit__). Remove unreliable __del__ cleanup in favor of explicit close() and aclose(). All fixes maintain backward compatibility while adding async-safe and multi-tenant-safe alternatives as required by project architecture. Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com>
|
@coderabbitai review |
|
/review |
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughThis PR adds async support with proper resource cleanup to auto-generator classes, makes the tool resolver safe for multi-tenant processes via eager path resolution and a reset function, and consolidates repeated Ollama daemon logic into a single shared helper module called by four existing methods. ChangesAsync Support, Tool Safety, and Ollama Consolidation
Sequence DiagramsequenceDiagram
participant Caller
participant AutoGenerator
participant BaseAutoGenerator
participant LiteLLM
participant AsyncOpenAI
Caller->>AutoGenerator: agenerate(merge=False)
AutoGenerator->>BaseAutoGenerator: _astructured_completion(prompt, TeamStructure)
BaseAutoGenerator->>BaseAutoGenerator: acquire _client_lock
alt LiteLLM available
BaseAutoGenerator->>LiteLLM: acompletion(...)
LiteLLM-->>BaseAutoGenerator: structured TeamStructure
else fallback to OpenAI
BaseAutoGenerator->>AsyncOpenAI: lazy init AsyncOpenAI()
BaseAutoGenerator->>AsyncOpenAI: beta.chat.completions.parse(...)
AsyncOpenAI-->>BaseAutoGenerator: structured TeamStructure
end
BaseAutoGenerator-->>AutoGenerator: TeamStructure
AutoGenerator->>AutoGenerator: convert_and_save(TeamStructure)
AutoGenerator-->>Caller: absolute output path
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@copilot Do a thorough review of this PR. Read ALL existing reviewer comments above from Qodo, Coderabbit, and Gemini first — incorporate their findings. Review areas:
|
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/55e92785-fa2d-40bb-b67a-0ef213025ab3 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/55e92785-fa2d-40bb-b67a-0ef213025ab3 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/55e92785-fa2d-40bb-b67a-0ef213025ab3 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
Completed a full pass and incorporated all currently available bot feedback. Qodo had no review (paused), CodeRabbit/Gemini had no actionable findings posted yet. I found and fixed one valid gap in this PR: async cleanup could leave a sync OpenAI client open in mixed-mode usage. Addressed in Focused validation run:
CI note: branch workflow runs are failing at workflow level with zero jobs started, so this currently looks infra/config-trigger side rather than test-execution failures from these code paths. |
|
@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding. Phase 1: Review per AGENTS.md
Phase 2: FIX Valid Issues Phase 3: Final Verdict |
|
Claude finished @MervinPraison's task —— View job ✅ Architecture Review CompleteActing as FINAL architecture reviewer for PR #1736. All requirements met per AGENTS.md guidelines. Todo List:
📋 Final Architecture Review✅ PHASE 1: AGENTS.md COMPLIANCEProtocol-driven architecture: ✅ PASSED
Backward compatibility: ✅ PASSED
Performance requirements: ✅ PASSED
✅ PHASE 2: REVIEWER FEEDBACK ANALYSISQodo: Paused (no issues raised)
✅ PHASE 3: TECHNICAL VALIDATION1. Ollama Blocking Bug: ✅ FIXED
2. Multi-tenant Tool Resolver: ✅ FIXED
3. Sync-only AutoGenerator: ✅ FIXED
Code Quality: ✅ EXCELLENT
🎯 FINAL VERDICT: ✅ APPROVEAll three critical bugs have been properly fixed with no architectural violations. The changes maintain full backward compatibility while adding robust async support and fixing blocking I/O issues. Copilot's additional fixes ensure proper resource cleanup in mixed sync/async scenarios. Recommendation: ✅ MERGE when CI passes |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/praisonai/praisonai/auto.py (1)
465-477: 💤 Low valueConsider cleaning up both clients in context managers for robustness.
The sync context manager (
__exit__) only closes the sync client, while the async context manager (__aexit__) only closes the async client. If a user mixes APIs (e.g., callsgenerate()thenagenerate()within the same instance), the non-matching client could leak when exiting.This is an edge case since the design assumes users stick to one paradigm, but for production robustness you could have each exit method attempt cleanup of both clients.
♻️ Optional enhancement
def __exit__(self, exc_type, exc, tb): self.close() + # Best-effort cleanup of async client if it was created + with self._client_lock: + async_client = self._async_openai_client + self._async_openai_client = None + if async_client is not None: + try: + async_client.close() # httpx async client supports sync close + except Exception: + pass # Best effort return False🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/praisonai/praisonai/auto.py` around lines 465 - 477, Update the context managers so both sync and async clients are always cleaned: in __exit__ call both self.close() and, if an async client exists, run its aclose() to completion (e.g., via asyncio.get_event_loop().run_until_complete(self.aclose()) or equivalent) and in __aexit__ await both self.aclose() and also ensure the sync client is closed (call self.close()); reference the existing methods __exit__, __aexit__, close(), and aclose() and make the cleanup attempts safe (check existence and handle/propagate exceptions consistently).src/praisonai/praisonai/train/_ollama.py (1)
32-71: 💤 Low valueReturned
Popenobject is ignored by all callers, leaving process management unclear.
ensure_ollama_runningreturns thePopenobject when it starts the daemon, butcreate_and_push_ollama_model(and all call sites) discards this return value. This creates ambiguity:
- If the caller wanted to terminate the daemon after model push, they can't.
- If the daemon should persist (typical for Ollama), returning
Popenserves no purpose.Consider either documenting that the daemon intentionally persists beyond the caller's lifetime (and returning
Nonealways), or providing an optional cleanup mechanism.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/praisonai/praisonai/train/_ollama.py` around lines 32 - 71, The function ensure_ollama_running currently returns a subprocess.Popen when it starts the daemon but callers like create_and_push_ollama_model ignore that value, so update ensure_ollama_running to make process lifetime explicit: add a boolean parameter (e.g., persist: bool = True or terminate_on_exit: bool = False) and document behavior, such that when persist is True the function returns None and leaves the daemon running, and when persist is False it starts the daemon and returns the Popen for caller cleanup (or internally terminates it after use); update create_and_push_ollama_model to call ensure_ollama_running with the intended flag or to capture and properly terminate the returned Popen, and adjust the docstring for ensure_ollama_running to describe the new parameter and return semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/praisonai/praisonai/train/_ollama.py`:
- Around line 86-88: The code currently writes the Modelfile to a hardcoded
"Modelfile" in the CWD (writing modelfile_content), which can race in
concurrent/multi-tenant runs; change this to create and write to a unique
temporary file (e.g., use tempfile.NamedTemporaryFile(delete=False) or
tempfile.mkstemp), write modelfile_content to that temp path, use that temp
filename wherever "Modelfile" was referenced, and ensure the temp file is
cleaned up after use (remove it in a finally block or context manager); update
any references to the literal "Modelfile" in this module (_ollama.py) to use the
temp filename variable.
---
Nitpick comments:
In `@src/praisonai/praisonai/auto.py`:
- Around line 465-477: Update the context managers so both sync and async
clients are always cleaned: in __exit__ call both self.close() and, if an async
client exists, run its aclose() to completion (e.g., via
asyncio.get_event_loop().run_until_complete(self.aclose()) or equivalent) and in
__aexit__ await both self.aclose() and also ensure the sync client is closed
(call self.close()); reference the existing methods __exit__, __aexit__,
close(), and aclose() and make the cleanup attempts safe (check existence and
handle/propagate exceptions consistently).
In `@src/praisonai/praisonai/train/_ollama.py`:
- Around line 32-71: The function ensure_ollama_running currently returns a
subprocess.Popen when it starts the daemon but callers like
create_and_push_ollama_model ignore that value, so update ensure_ollama_running
to make process lifetime explicit: add a boolean parameter (e.g., persist: bool
= True or terminate_on_exit: bool = False) and document behavior, such that when
persist is True the function returns None and leaves the daemon running, and
when persist is False it starts the daemon and returns the Popen for caller
cleanup (or internally terminates it after use); update
create_and_push_ollama_model to call ensure_ollama_running with the intended
flag or to capture and properly terminate the returned Popen, and adjust the
docstring for ensure_ollama_running to describe the new parameter and return
semantics.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: ff23eae5-7c6b-4b70-84f2-481edfb77af9
📒 Files selected for processing (7)
src/praisonai/praisonai/auto.pysrc/praisonai/praisonai/tool_resolver.pysrc/praisonai/praisonai/train.pysrc/praisonai/praisonai/train/_ollama.pysrc/praisonai/praisonai/train/llm/trainer.pysrc/praisonai/praisonai/train_vision.pysrc/praisonai/praisonai/upload_vision.py
| # Write Modelfile | ||
| with open("Modelfile", "w") as f: | ||
| f.write(modelfile_content) |
There was a problem hiding this comment.
Modelfile written to CWD may conflict in concurrent or multi-tenant scenarios.
Writing to a hardcoded "Modelfile" path in the current working directory can cause race conditions if multiple processes invoke this function concurrently. Consider using a temporary file with tempfile.NamedTemporaryFile or a unique filename.
🛠️ Proposed fix using tempfile
+import tempfile
+
def create_and_push_ollama_model(ollama_model: str, model_parameters: str, modelfile_content: str) -> None:
...
- # Write Modelfile
- with open("Modelfile", "w") as f:
- f.write(modelfile_content)
+ # Write Modelfile to a temporary file
+ with tempfile.NamedTemporaryFile(mode="w", suffix="_Modelfile", delete=False) as f:
+ f.write(modelfile_content)
+ modelfile_path = f.name
# Ensure daemon is running
ensure_ollama_running()
# Create and push model
tag = f"{ollama_model}:{model_parameters}"
- subprocess.run(["ollama", "create", tag, "-f", "Modelfile"], check=True)
+ subprocess.run(["ollama", "create", tag, "-f", modelfile_path], check=True)
subprocess.run(["ollama", "push", tag], check=True)
+
+ # Cleanup
+ os.unlink(modelfile_path)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/praisonai/praisonai/train/_ollama.py` around lines 86 - 88, The code
currently writes the Modelfile to a hardcoded "Modelfile" in the CWD (writing
modelfile_content), which can race in concurrent/multi-tenant runs; change this
to create and write to a unique temporary file (e.g., use
tempfile.NamedTemporaryFile(delete=False) or tempfile.mkstemp), write
modelfile_content to that temp path, use that temp filename wherever "Modelfile"
was referenced, and ensure the temp file is cleaned up after use (remove it in a
finally block or context manager); update any references to the literal
"Modelfile" in this module (_ollama.py) to use the temp filename variable.
Greptile SummaryThis PR addresses three bugs: a blocking
Confidence Score: 2/5Not safe to merge — the Ollama daemon fix, the headline change of this PR, will raise ImportError every time a user tries to create or push an Ollama model via the CLI. Both train.py and train_vision.py are launched as standalone scripts by the CLI. The newly introduced relative imports inside create_and_push_ollama_model() are invalid in that execution context, meaning the method raises immediately instead of running the fixed daemon-management logic. src/praisonai/praisonai/train.py and src/praisonai/praisonai/train_vision.py — the relative imports must be replaced before merging. Important Files Changed
Reviews (1): Last reviewed commit: "refactor(auto): clarify async cleanup va..." | Re-trigger Greptile |
| def create_and_push_ollama_model(self): | ||
| from ._ollama import create_and_push_ollama_model | ||
| modelfile_content = self.prepare_modelfile_content() | ||
| with open("Modelfile", "w") as file: | ||
| file.write(modelfile_content) | ||
| subprocess.run(["ollama", "serve"]) | ||
| subprocess.run(["ollama", "create", f"{self.config['ollama_model']}:{self.config['model_parameters']}", "-f", "Modelfile"]) | ||
| subprocess.run(["ollama", "push", f"{self.config['ollama_model']}:{self.config['model_parameters']}"]) | ||
| create_and_push_ollama_model( | ||
| self.config['ollama_model'], | ||
| self.config['model_parameters'], | ||
| modelfile_content | ||
| ) |
There was a problem hiding this comment.
Relative import breaks when file is executed as script
train.py is invoked by the CLI as a subprocess script (python -u train.py train) via cli/main.py line 730. When run as __main__, relative imports raise ImportError: attempted relative import with no known parent package, making create_and_push_ollama_model() always fail. The Ollama blocking fix is completely unreachable through the primary CLI code path.
The correct approach for a script file is to use an absolute/path-based import, e.g. importlib.util.spec_from_file_location pointed at os.path.join(os.path.dirname(os.path.abspath(__file__)), "train", "_ollama.py"), or to factor the helper call into the train/__init__.py and adjust the CLI to import the module instead of executing it as a script.
| def create_and_push_ollama_model(self): | ||
| from .train._ollama import create_and_push_ollama_model | ||
| modelfile_content = self.prepare_modelfile_content() | ||
| with open("Modelfile", "w") as file: | ||
| file.write(modelfile_content) | ||
| subprocess.run(["ollama", "serve"]) | ||
| subprocess.run(["ollama", "create", f"{self.config['ollama_model']}:{self.config['model_parameters']}", "-f", "Modelfile"]) | ||
| subprocess.run(["ollama", "push", f"{self.config['ollama_model']}:{self.config['model_parameters']}"]) | ||
| create_and_push_ollama_model( | ||
| self.config['ollama_model'], | ||
| self.config['model_parameters'], | ||
| modelfile_content | ||
| ) |
There was a problem hiding this comment.
Same relative-import failure as
train.py
train_vision.py is also run as a subprocess script by the CLI (python -u train_vision.py train). from .train._ollama import ... requires Python to know the parent package, but when executed as __main__ there is no parent package and the import raises ImportError: attempted relative import with no known parent package. Every call to create_and_push_ollama_model() in the vision training path will fail at this line.
| if _check_openai_available(): | ||
| if self._async_openai_client is None: | ||
| with self._client_lock: | ||
| if self._async_openai_client is None: | ||
| try: | ||
| from openai import AsyncOpenAI | ||
| except ImportError as e: | ||
| raise ImportError("Install with: pip install openai") from e | ||
| cfg = self.config_list[0] | ||
| self._async_openai_client = AsyncOpenAI( | ||
| api_key=cfg.get("api_key") or os.environ.get("OPENAI_API_KEY"), | ||
| base_url=cfg.get("base_url"), | ||
| ) | ||
|
|
There was a problem hiding this comment.
Sync
threading.Lock blocks the event loop under contention in async context
self._client_lock is a threading.Lock. Inside an async method, if two coroutines concurrently pass the outer None check and then race to with self._client_lock:, the second coroutine blocks the entire event loop thread until the lock is released. A dedicated asyncio.Lock for the async client would be the idiomatic fix.
Summary
This PR fixes three critical bugs in the PraisonAI wrapper layer that violate the project's "async-safe by default" and "production-ready" principles, as identified in issue #1735.
Changes Made
1. Fix Ollama Blocking Bug ✅
Problem: subprocess.run(["ollama", "serve"]) blocks forever, making create and push commands unreachable.
Solution:
2. Fix Multi-tenant Unsafe Tool Resolver ✅
Problem: Process-wide singleton binds tools to first caller's CWD, breaking multi-tenant gateways.
Solution:
3. Fix Sync-only AutoGenerator ✅
Problem: No async API and unreliable del cleanup causes socket leaks in long-running servers.
Solution:
Backward Compatibility
All changes maintain full backward compatibility:
Testing
Architecture Compliance
These fixes align with PraisonAI's core principles:
Fixes #1735
Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Chores