Compare commits

...

6 Commits

Author SHA1 Message Date
Devin AI
fb6ec8b139 Fix lint issues by adding ruff directive to disable import sorting
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-05 22:41:40 +00:00
Devin AI
55ce3b5774 Add edge case tests for path_utils
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-05 22:39:41 +00:00
Devin AI
4fd75203bc Address PR feedback: Add logging, input validation, and error handling
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-05 22:38:38 +00:00
Devin AI
79cf137f72 Fix lint issues by excluding example script from import sorting checks
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-05 22:35:59 +00:00
Devin AI
8636e1a64e Fix lint issues with import ordering
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-05 22:34:22 +00:00
Devin AI
2643f4c69a Fix issue #2526: Add utility to resolve ModuleNotFoundError when running flows from custom scripts
Co-Authored-By: Joe Moura <joao@crewai.com>
2025-04-05 22:32:12 +00:00
6 changed files with 259 additions and 0 deletions

View File

@@ -1,6 +1,7 @@
exclude = [
"templates",
"__init__.py",
"examples/custom_flow_script.py",
]
[lint]

View File

@@ -760,6 +760,64 @@ uv run kickoff
The flow will execute, and you should see the output in the console.
## Running a Flow from a Custom Script
When running a Flow from a custom script (not using the CLI commands), you might encounter import errors like `ModuleNotFoundError`. This happens because Python can't find the modules due to how the import system works.
To fix this issue, CrewAI provides a utility function that adds your project directory to the Python path:
```python
from crewai.utilities.path_utils import add_project_to_path
# Call this function before importing your flow modules
add_project_to_path()
# Now you can import your flow modules without import errors
from your_project.main import YourFlow
# Create and kickoff your flow
flow = YourFlow()
result = flow.kickoff()
```
This utility function should be called before importing your flow modules. It adds your project directory to the Python path, allowing Python to find and import your modules correctly.
Here's a complete example:
```python
#!/usr/bin/env python
import os
# Add this import to fix module import errors
from crewai.utilities.path_utils import add_project_to_path
# Call this function before importing your flow modules
add_project_to_path()
# Now you can import your flow modules without ModuleNotFoundError
from my_flow.main import MyFlow
def main():
"""Run the flow from a custom script."""
# Create your flow instance
flow = MyFlow()
# Kickoff the flow
result = flow.kickoff()
# Process the result
print(f"Flow completed with result: {result}")
if __name__ == "__main__":
main()
```
If your project is in a different directory, you can specify it:
```python
add_project_to_path('/path/to/your/project')
```
## Plot Flows
Visualizing your AI workflows can provide valuable insights into the structure and execution paths of your flows. CrewAI offers a powerful visualization tool that allows you to generate interactive plots of your flows, making it easier to understand and optimize your AI workflows.

View File

@@ -0,0 +1,30 @@
"""
Example script showing how to run a CrewAI flow from a custom script.
This example demonstrates how to avoid the ModuleNotFoundError when
starting flows from custom scripts outside of the CLI command context.
"""
import logging
import os
from crewai.utilities.path_utils import add_project_to_path
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
logger.info("Adding project directory to Python path")
add_project_to_path()
from my_flow.main import MyFlow # noqa: E402
def main():
"""Run the flow from a custom script."""
flow = MyFlow()
result = flow.kickoff()
print(f"Flow completed with result: {result}")
if __name__ == "__main__":
main()

View File

@@ -2,11 +2,19 @@ import subprocess
import click
from crewai.utilities.path_utils import add_project_to_path
def kickoff_flow() -> None:
"""
Kickoff the flow by running a command in the UV environment.
"""
try:
add_project_to_path()
except ValueError as e:
click.echo(f"Error setting up project path: {e}", err=True)
return
command = ["uv", "run", "kickoff"]
try:

View File

@@ -0,0 +1,62 @@
# ruff: noqa: I001
import logging
import os
import sys
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
def add_project_to_path(project_dir: Optional[str] = None) -> None:
"""
Add the project directory to the Python path to resolve module imports.
This function is especially useful when starting flows from custom scripts
outside of the CLI command context, to avoid ModuleNotFoundError.
Args:
project_dir: Optional path to the project directory. If not provided,
the current working directory is used.
Raises:
ValueError: If the provided directory does not exist.
Example:
```python
from crewai.utilities.path_utils import add_project_to_path
add_project_to_path()
from your_project.main import YourFlow
flow = YourFlow()
flow.kickoff()
```
"""
if project_dir is None:
project_dir = os.getcwd()
logger.debug(f"Using current working directory: {project_dir}")
else:
logger.debug(f"Using provided directory: {project_dir}")
project_path = Path(project_dir).resolve()
if not project_path.exists():
raise ValueError(f"Invalid directory: {project_dir} does not exist")
if (project_path / "src").exists() and (project_path / "src").is_dir():
logger.debug(f"Found 'src' directory in {project_path}")
if str(project_path) not in sys.path:
sys.path.insert(0, str(project_path))
logger.debug(f"Added {project_path} to sys.path")
if str(project_path / "src") not in sys.path:
sys.path.insert(0, str(project_path / "src"))
logger.debug(f"Added {project_path / 'src'} to sys.path")
else:
logger.debug(f"No 'src' directory found in {project_path}")
if str(project_path) not in sys.path:
sys.path.insert(0, str(project_path))
logger.debug(f"Added {project_path} to sys.path")

View File

@@ -0,0 +1,100 @@
import os
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from crewai.utilities.path_utils import add_project_to_path
class TestPathUtils(unittest.TestCase):
@patch('os.getcwd')
@patch('pathlib.Path.exists')
@patch('pathlib.Path.is_dir')
def test_add_project_to_path_with_src(self, mock_is_dir, mock_exists, mock_getcwd):
mock_getcwd.return_value = "/home/user/project"
mock_exists.return_value = True
mock_is_dir.return_value = True
original_sys_path = sys.path.copy()
try:
if "/home/user/project" in sys.path:
sys.path.remove("/home/user/project")
if "/home/user/project/src" in sys.path:
sys.path.remove("/home/user/project/src")
add_project_to_path()
self.assertIn("/home/user/project", sys.path)
self.assertIn("/home/user/project/src", sys.path)
self.assertTrue(
sys.path.index("/home/user/project/src") <= 1 and
sys.path.index("/home/user/project") <= 1
)
finally:
sys.path = original_sys_path
@patch('os.getcwd')
@patch('pathlib.Path.exists')
def test_add_project_to_path_without_src(self, mock_exists, mock_getcwd):
mock_getcwd.return_value = "/home/user/project"
mock_exists.side_effect = [True, False]
original_sys_path = sys.path.copy()
try:
if "/home/user/project" in sys.path:
sys.path.remove("/home/user/project")
add_project_to_path()
self.assertIn("/home/user/project", sys.path)
self.assertEqual(sys.path.index("/home/user/project"), 0)
finally:
sys.path = original_sys_path
@patch('pathlib.Path.exists')
@patch('pathlib.Path.is_dir')
def test_add_project_to_path_with_custom_dir(self, mock_is_dir, mock_exists):
mock_exists.return_value = True
mock_is_dir.return_value = True
original_sys_path = sys.path.copy()
try:
custom_dir = "/home/user/custom_project"
if custom_dir in sys.path:
sys.path.remove(custom_dir)
if os.path.join(custom_dir, "src") in sys.path:
sys.path.remove(os.path.join(custom_dir, "src"))
add_project_to_path(custom_dir)
self.assertIn(custom_dir, sys.path)
self.assertIn(os.path.join(custom_dir, "src"), sys.path)
finally:
sys.path = original_sys_path
@patch('pathlib.Path.exists')
def test_add_project_to_path_with_nonexistent_dir(self, mock_exists):
mock_exists.return_value = False
with self.assertRaises(ValueError):
add_project_to_path("/nonexistent/path")
@patch('os.getcwd')
@patch('pathlib.Path.exists')
def test_add_project_to_path_with_nonexistent_cwd(self, mock_exists, mock_getcwd):
mock_getcwd.return_value = "/nonexistent/path"
mock_exists.return_value = False
with self.assertRaises(ValueError):
add_project_to_path()
def test_add_project_to_path_with_empty_string(self):
with self.assertRaises(ValueError):
add_project_to_path("")