Lorenze/new version 0.140.0 (#3106)
Some checks failed
Notify Downstream / notify-downstream (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled

* fix: clean up whitespace and update dependencies

* Removed unnecessary whitespace in multiple files for consistency.
* Updated `crewai-tools` dependency version to `0.49.0` in `pyproject.toml` and related template files.
* Bumped CrewAI version to `0.140.0` in `__init__.py` for alignment with updated dependencies.

* chore: update pyproject.toml to exclude documentation from build targets

* Added exclusions for the `docs` directory in both wheel and sdist build targets to streamline the build process and reduce unnecessary file inclusion.

* chore: update uv.lock for dependency resolution and Python version compatibility

* Incremented revision to 2.
* Updated resolution markers to include support for Python 3.13 and adjusted platform checks for better compatibility.
* Added new wheel URLs for zstandard version 0.23.0 to ensure availability across various platforms.

* chore: pin json-repair dependency version in pyproject.toml and uv.lock

* Updated json-repair dependency from a range to a specific version (0.25.2) for consistency and to avoid potential compatibility issues.
* Adjusted related entries in uv.lock to reflect the pinned version, ensuring alignment across project files.

* chore: pin agentops dependency version in pyproject.toml and uv.lock

* Updated agentops dependency from a range to a specific version (0.3.18) for consistency and to avoid potential compatibility issues.
* Adjusted related entries in uv.lock to reflect the pinned version, ensuring alignment across project files.

* test: enhance cache call assertions in crew tests

* Improved the test for cache hitting between agents by filtering mock calls to ensure they include the expected 'tool' and 'input' keywords.
* Added assertions to verify the number of cache calls and their expected arguments, enhancing the reliability of the test.
* Cleaned up whitespace and improved readability in various test cases for better maintainability.
This commit is contained in:
Lorenze Jay
2025-07-02 15:22:18 -07:00
committed by GitHub
parent a77dcdd419
commit 748c25451c
9 changed files with 3460 additions and 2432 deletions

View File

@@ -260,7 +260,7 @@ def handle_success(self):
# Handle success case # Handle success case
pass pass
@listen("failure_path") @listen("failure_path")
def handle_failure(self): def handle_failure(self):
# Handle failure case # Handle failure case
pass pass
@@ -288,7 +288,7 @@ class SelectiveFlow(Flow):
def critical_step(self): def critical_step(self):
# Only this method's state is persisted # Only this method's state is persisted
self.state["important_data"] = "value" self.state["important_data"] = "value"
@start() @start()
def temporary_step(self): def temporary_step(self):
# This method's state is not persisted # This method's state is not persisted
@@ -322,20 +322,20 @@ flow.plot("workflow_diagram") # Generates HTML visualization
class CyclicFlow(Flow): class CyclicFlow(Flow):
max_iterations = 5 max_iterations = 5
current_iteration = 0 current_iteration = 0
@start("loop") @start("loop")
def process_iteration(self): def process_iteration(self):
if self.current_iteration >= self.max_iterations: if self.current_iteration >= self.max_iterations:
return return
# Process current iteration # Process current iteration
self.current_iteration += 1 self.current_iteration += 1
@router(process_iteration) @router(process_iteration)
def check_continue(self): def check_continue(self):
if self.current_iteration < self.max_iterations: if self.current_iteration < self.max_iterations:
return "loop" # Continue cycling return "loop" # Continue cycling
return "complete" return "complete"
@listen("complete") @listen("complete")
def finalize(self): def finalize(self):
# Final processing # Final processing
@@ -369,7 +369,7 @@ def risky_operation(self):
self.state["success"] = False self.state["success"] = False
return None return None
@listen(risky_operation) @listen(risky_operation)
def handle_result(self, result): def handle_result(self, result):
if self.state.get("success", False): if self.state.get("success", False):
# Handle success case # Handle success case
@@ -390,7 +390,7 @@ class CrewOrchestrationFlow(Flow[WorkflowState]):
result = research_crew.crew().kickoff(inputs={"topic": self.state.research_topic}) result = research_crew.crew().kickoff(inputs={"topic": self.state.research_topic})
self.state.research_results = result.raw self.state.research_results = result.raw
return result return result
@listen(research_phase) @listen(research_phase)
def analysis_phase(self, research_results): def analysis_phase(self, research_results):
analysis_crew = AnalysisCrew() analysis_crew = AnalysisCrew()
@@ -400,13 +400,13 @@ class CrewOrchestrationFlow(Flow[WorkflowState]):
}) })
self.state.analysis_results = result.raw self.state.analysis_results = result.raw
return result return result
@router(analysis_phase) @router(analysis_phase)
def decide_next_action(self): def decide_next_action(self):
if self.state.analysis_results.confidence > 0.7: if self.state.analysis_results.confidence > 0.7:
return "generate_report" return "generate_report"
return "additional_research" return "additional_research"
@listen("generate_report") @listen("generate_report")
def final_report(self): def final_report(self):
reporting_crew = ReportingCrew() reporting_crew = ReportingCrew()
@@ -439,7 +439,7 @@ class CrewOrchestrationFlow(Flow[WorkflowState]):
## CrewAI Version Compatibility: ## CrewAI Version Compatibility:
- Stay updated with CrewAI releases for new features and bug fixes - Stay updated with CrewAI releases for new features and bug fixes
- Test crew functionality when upgrading CrewAI versions - Test crew functionality when upgrading CrewAI versions
- Use version constraints in pyproject.toml (e.g., "crewai[tools]>=0.134.0,<1.0.0") - Use version constraints in pyproject.toml (e.g., "crewai[tools]>=0.140.0,<1.0.0")
- Monitor deprecation warnings for future compatibility - Monitor deprecation warnings for future compatibility
## Code Examples and Implementation Patterns ## Code Examples and Implementation Patterns
@@ -464,22 +464,22 @@ class ResearchOutput(BaseModel):
@CrewBase @CrewBase
class ResearchCrew(): class ResearchCrew():
"""Advanced research crew with structured outputs and validation""" """Advanced research crew with structured outputs and validation"""
agents: List[BaseAgent] agents: List[BaseAgent]
tasks: List[Task] tasks: List[Task]
@before_kickoff @before_kickoff
def setup_environment(self): def setup_environment(self):
"""Initialize environment before crew execution""" """Initialize environment before crew execution"""
print("🚀 Setting up research environment...") print("🚀 Setting up research environment...")
# Validate API keys, create directories, etc. # Validate API keys, create directories, etc.
@after_kickoff @after_kickoff
def cleanup_and_report(self, output): def cleanup_and_report(self, output):
"""Handle post-execution tasks""" """Handle post-execution tasks"""
print(f"✅ Research completed. Generated {len(output.tasks_output)} task outputs") print(f"✅ Research completed. Generated {len(output.tasks_output)} task outputs")
print(f"📊 Token usage: {output.token_usage}") print(f"📊 Token usage: {output.token_usage}")
@agent @agent
def researcher(self) -> Agent: def researcher(self) -> Agent:
return Agent( return Agent(
@@ -490,7 +490,7 @@ class ResearchCrew():
max_iter=15, max_iter=15,
max_execution_time=1800 max_execution_time=1800
) )
@agent @agent
def analyst(self) -> Agent: def analyst(self) -> Agent:
return Agent( return Agent(
@@ -499,7 +499,7 @@ class ResearchCrew():
verbose=True, verbose=True,
memory=True memory=True
) )
@task @task
def research_task(self) -> Task: def research_task(self) -> Task:
return Task( return Task(
@@ -507,7 +507,7 @@ class ResearchCrew():
agent=self.researcher(), agent=self.researcher(),
output_pydantic=ResearchOutput output_pydantic=ResearchOutput
) )
@task @task
def validation_task(self) -> Task: def validation_task(self) -> Task:
return Task( return Task(
@@ -517,7 +517,7 @@ class ResearchCrew():
guardrail=self.validate_research_quality, guardrail=self.validate_research_quality,
max_retries=3 max_retries=3
) )
def validate_research_quality(self, output) -> tuple[bool, str]: def validate_research_quality(self, output) -> tuple[bool, str]:
"""Custom guardrail to ensure research quality""" """Custom guardrail to ensure research quality"""
content = output.raw content = output.raw
@@ -526,7 +526,7 @@ class ResearchCrew():
if not any(keyword in content.lower() for keyword in ['conclusion', 'finding', 'result']): if not any(keyword in content.lower() for keyword in ['conclusion', 'finding', 'result']):
return False, "Missing key analytical elements." return False, "Missing key analytical elements."
return True, content return True, content
@crew @crew
def crew(self) -> Crew: def crew(self) -> Crew:
return Crew( return Crew(
@@ -557,13 +557,13 @@ class RobustSearchTool(BaseTool):
name: str = "robust_search" name: str = "robust_search"
description: str = "Perform web search with retry logic and error handling" description: str = "Perform web search with retry logic and error handling"
args_schema: Type[BaseModel] = SearchInput args_schema: Type[BaseModel] = SearchInput
def __init__(self, api_key: Optional[str] = None, **kwargs): def __init__(self, api_key: Optional[str] = None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.api_key = api_key or os.getenv("SEARCH_API_KEY") self.api_key = api_key or os.getenv("SEARCH_API_KEY")
self.rate_limit_delay = 1.0 self.rate_limit_delay = 1.0
self.last_request_time = 0 self.last_request_time = 0
@retry( @retry(
stop=stop_after_attempt(3), stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10) wait=wait_exponential(multiplier=1, min=4, max=10)
@@ -575,43 +575,43 @@ class RobustSearchTool(BaseTool):
time_since_last = time.time() - self.last_request_time time_since_last = time.time() - self.last_request_time
if time_since_last < self.rate_limit_delay: if time_since_last < self.rate_limit_delay:
time.sleep(self.rate_limit_delay - time_since_last) time.sleep(self.rate_limit_delay - time_since_last)
# Input validation # Input validation
if not query or len(query.strip()) == 0: if not query or len(query.strip()) == 0:
return "Error: Empty search query provided" return "Error: Empty search query provided"
if len(query) > 500: if len(query) > 500:
return "Error: Search query too long (max 500 characters)" return "Error: Search query too long (max 500 characters)"
# Perform search # Perform search
results = self._perform_search(query, max_results, timeout) results = self._perform_search(query, max_results, timeout)
self.last_request_time = time.time() self.last_request_time = time.time()
return self._format_results(results) return self._format_results(results)
except requests.exceptions.Timeout: except requests.exceptions.Timeout:
return f"Search timed out after {timeout} seconds" return f"Search timed out after {timeout} seconds"
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
return f"Search failed due to network error: {str(e)}" return f"Search failed due to network error: {str(e)}"
except Exception as e: except Exception as e:
return f"Unexpected error during search: {str(e)}" return f"Unexpected error during search: {str(e)}"
def _perform_search(self, query: str, max_results: int, timeout: int) -> List[dict]: def _perform_search(self, query: str, max_results: int, timeout: int) -> List[dict]:
"""Implement actual search logic here""" """Implement actual search logic here"""
# Your search API implementation # Your search API implementation
pass pass
def _format_results(self, results: List[dict]) -> str: def _format_results(self, results: List[dict]) -> str:
"""Format search results for LLM consumption""" """Format search results for LLM consumption"""
if not results: if not results:
return "No results found for the given query." return "No results found for the given query."
formatted = "Search Results:\n\n" formatted = "Search Results:\n\n"
for i, result in enumerate(results[:10], 1): for i, result in enumerate(results[:10], 1):
formatted += f"{i}. {result.get('title', 'No title')}\n" formatted += f"{i}. {result.get('title', 'No title')}\n"
formatted += f" URL: {result.get('url', 'No URL')}\n" formatted += f" URL: {result.get('url', 'No URL')}\n"
formatted += f" Summary: {result.get('snippet', 'No summary')}\n\n" formatted += f" Summary: {result.get('snippet', 'No summary')}\n\n"
return formatted return formatted
``` ```
@@ -623,20 +623,20 @@ from crewai.memory.storage.mem0_storage import Mem0Storage
class AdvancedMemoryManager: class AdvancedMemoryManager:
"""Enhanced memory management for CrewAI applications""" """Enhanced memory management for CrewAI applications"""
def __init__(self, crew, config: dict = None): def __init__(self, crew, config: dict = None):
self.crew = crew self.crew = crew
self.config = config or {} self.config = config or {}
self.setup_memory_systems() self.setup_memory_systems()
def setup_memory_systems(self): def setup_memory_systems(self):
"""Configure multiple memory systems""" """Configure multiple memory systems"""
# Short-term memory for current session # Short-term memory for current session
self.short_term = ShortTermMemory() self.short_term = ShortTermMemory()
# Long-term memory for cross-session persistence # Long-term memory for cross-session persistence
self.long_term = LongTermMemory() self.long_term = LongTermMemory()
# External memory with Mem0 (if configured) # External memory with Mem0 (if configured)
if self.config.get('use_external_memory'): if self.config.get('use_external_memory'):
self.external = ExternalMemory.create_storage( self.external = ExternalMemory.create_storage(
@@ -649,8 +649,8 @@ class AdvancedMemoryManager:
} }
} }
) )
def save_with_context(self, content: str, memory_type: str = "short_term", def save_with_context(self, content: str, memory_type: str = "short_term",
metadata: dict = None, agent: str = None): metadata: dict = None, agent: str = None):
"""Save content with enhanced metadata""" """Save content with enhanced metadata"""
enhanced_metadata = { enhanced_metadata = {
@@ -659,14 +659,14 @@ class AdvancedMemoryManager:
"crew_type": self.crew.__class__.__name__, "crew_type": self.crew.__class__.__name__,
**(metadata or {}) **(metadata or {})
} }
if memory_type == "short_term": if memory_type == "short_term":
self.short_term.save(content, enhanced_metadata, agent) self.short_term.save(content, enhanced_metadata, agent)
elif memory_type == "long_term": elif memory_type == "long_term":
self.long_term.save(content, enhanced_metadata, agent) self.long_term.save(content, enhanced_metadata, agent)
elif memory_type == "external" and hasattr(self, 'external'): elif memory_type == "external" and hasattr(self, 'external'):
self.external.save(content, enhanced_metadata, agent) self.external.save(content, enhanced_metadata, agent)
def search_across_memories(self, query: str, limit: int = 5) -> dict: def search_across_memories(self, query: str, limit: int = 5) -> dict:
"""Search across all memory systems""" """Search across all memory systems"""
results = { results = {
@@ -674,23 +674,23 @@ class AdvancedMemoryManager:
"long_term": [], "long_term": [],
"external": [] "external": []
} }
# Search short-term memory # Search short-term memory
results["short_term"] = self.short_term.search(query, limit=limit) results["short_term"] = self.short_term.search(query, limit=limit)
# Search long-term memory # Search long-term memory
results["long_term"] = self.long_term.search(query, limit=limit) results["long_term"] = self.long_term.search(query, limit=limit)
# Search external memory (if available) # Search external memory (if available)
if hasattr(self, 'external'): if hasattr(self, 'external'):
results["external"] = self.external.search(query, limit=limit) results["external"] = self.external.search(query, limit=limit)
return results return results
def cleanup_old_memories(self, days_threshold: int = 30): def cleanup_old_memories(self, days_threshold: int = 30):
"""Clean up old memories based on age""" """Clean up old memories based on age"""
cutoff_time = time.time() - (days_threshold * 24 * 60 * 60) cutoff_time = time.time() - (days_threshold * 24 * 60 * 60)
# Implement cleanup logic based on timestamps in metadata # Implement cleanup logic based on timestamps in metadata
# This would vary based on your specific storage implementation # This would vary based on your specific storage implementation
pass pass
@@ -719,12 +719,12 @@ class TaskMetrics:
class CrewMonitor: class CrewMonitor:
"""Comprehensive monitoring for CrewAI applications""" """Comprehensive monitoring for CrewAI applications"""
def __init__(self, crew_name: str, log_level: str = "INFO"): def __init__(self, crew_name: str, log_level: str = "INFO"):
self.crew_name = crew_name self.crew_name = crew_name
self.metrics: List[TaskMetrics] = [] self.metrics: List[TaskMetrics] = []
self.session_start = time.time() self.session_start = time.time()
# Setup logging # Setup logging
logging.basicConfig( logging.basicConfig(
level=getattr(logging, log_level), level=getattr(logging, log_level),
@@ -735,7 +735,7 @@ class CrewMonitor:
] ]
) )
self.logger = logging.getLogger(f"CrewAI.{crew_name}") self.logger = logging.getLogger(f"CrewAI.{crew_name}")
def start_task_monitoring(self, task_name: str, agent_name: str) -> dict: def start_task_monitoring(self, task_name: str, agent_name: str) -> dict:
"""Start monitoring a task execution""" """Start monitoring a task execution"""
context = { context = {
@@ -743,16 +743,16 @@ class CrewMonitor:
"agent_name": agent_name, "agent_name": agent_name,
"start_time": time.time() "start_time": time.time()
} }
self.logger.info(f"Task started: {task_name} by {agent_name}") self.logger.info(f"Task started: {task_name} by {agent_name}")
return context return context
def end_task_monitoring(self, context: dict, success: bool = True, def end_task_monitoring(self, context: dict, success: bool = True,
tokens_used: int = 0, error: str = None): tokens_used: int = 0, error: str = None):
"""End monitoring and record metrics""" """End monitoring and record metrics"""
end_time = time.time() end_time = time.time()
duration = end_time - context["start_time"] duration = end_time - context["start_time"]
# Get memory usage (if psutil is available) # Get memory usage (if psutil is available)
memory_usage = None memory_usage = None
try: try:
@@ -761,7 +761,7 @@ class CrewMonitor:
memory_usage = process.memory_info().rss / 1024 / 1024 # MB memory_usage = process.memory_info().rss / 1024 / 1024 # MB
except ImportError: except ImportError:
pass pass
metrics = TaskMetrics( metrics = TaskMetrics(
task_name=context["task_name"], task_name=context["task_name"],
agent_name=context["agent_name"], agent_name=context["agent_name"],
@@ -773,29 +773,29 @@ class CrewMonitor:
error_message=error, error_message=error,
memory_usage_mb=memory_usage memory_usage_mb=memory_usage
) )
self.metrics.append(metrics) self.metrics.append(metrics)
# Log the completion # Log the completion
status = "SUCCESS" if success else "FAILED" status = "SUCCESS" if success else "FAILED"
self.logger.info(f"Task {status}: {context['task_name']} " self.logger.info(f"Task {status}: {context['task_name']} "
f"(Duration: {duration:.2f}s, Tokens: {tokens_used})") f"(Duration: {duration:.2f}s, Tokens: {tokens_used})")
if error: if error:
self.logger.error(f"Task error: {error}") self.logger.error(f"Task error: {error}")
def get_performance_summary(self) -> Dict[str, Any]: def get_performance_summary(self) -> Dict[str, Any]:
"""Generate comprehensive performance summary""" """Generate comprehensive performance summary"""
if not self.metrics: if not self.metrics:
return {"message": "No metrics recorded yet"} return {"message": "No metrics recorded yet"}
successful_tasks = [m for m in self.metrics if m.success] successful_tasks = [m for m in self.metrics if m.success]
failed_tasks = [m for m in self.metrics if not m.success] failed_tasks = [m for m in self.metrics if not m.success]
total_duration = sum(m.duration for m in self.metrics) total_duration = sum(m.duration for m in self.metrics)
total_tokens = sum(m.tokens_used for m in self.metrics) total_tokens = sum(m.tokens_used for m in self.metrics)
avg_duration = total_duration / len(self.metrics) avg_duration = total_duration / len(self.metrics)
return { return {
"crew_name": self.crew_name, "crew_name": self.crew_name,
"session_duration": time.time() - self.session_start, "session_duration": time.time() - self.session_start,
@@ -811,7 +811,7 @@ class CrewMonitor:
"most_token_intensive": max(self.metrics, key=lambda x: x.tokens_used).task_name if self.metrics else None, "most_token_intensive": max(self.metrics, key=lambda x: x.tokens_used).task_name if self.metrics else None,
"common_errors": self._get_common_errors() "common_errors": self._get_common_errors()
} }
def _get_common_errors(self) -> Dict[str, int]: def _get_common_errors(self) -> Dict[str, int]:
"""Get frequency of common errors""" """Get frequency of common errors"""
error_counts = {} error_counts = {}
@@ -819,20 +819,20 @@ class CrewMonitor:
if metric.error_message: if metric.error_message:
error_counts[metric.error_message] = error_counts.get(metric.error_message, 0) + 1 error_counts[metric.error_message] = error_counts.get(metric.error_message, 0) + 1
return dict(sorted(error_counts.items(), key=lambda x: x[1], reverse=True)) return dict(sorted(error_counts.items(), key=lambda x: x[1], reverse=True))
def export_metrics(self, filename: str = None) -> str: def export_metrics(self, filename: str = None) -> str:
"""Export metrics to JSON file""" """Export metrics to JSON file"""
if not filename: if not filename:
filename = f"crew_metrics_{self.crew_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" filename = f"crew_metrics_{self.crew_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
export_data = { export_data = {
"summary": self.get_performance_summary(), "summary": self.get_performance_summary(),
"detailed_metrics": [asdict(m) for m in self.metrics] "detailed_metrics": [asdict(m) for m in self.metrics]
} }
with open(filename, 'w') as f: with open(filename, 'w') as f:
json.dump(export_data, f, indent=2, default=str) json.dump(export_data, f, indent=2, default=str)
self.logger.info(f"Metrics exported to {filename}") self.logger.info(f"Metrics exported to {filename}")
return filename return filename
@@ -847,10 +847,10 @@ def monitored_research_task(self) -> Task:
if context: if context:
tokens = getattr(task_output, 'token_usage', {}).get('total', 0) tokens = getattr(task_output, 'token_usage', {}).get('total', 0)
monitor.end_task_monitoring(context, success=True, tokens_used=tokens) monitor.end_task_monitoring(context, success=True, tokens_used=tokens)
# Start monitoring would be called before task execution # Start monitoring would be called before task execution
# This is a simplified example - in practice you'd integrate this into the task execution flow # This is a simplified example - in practice you'd integrate this into the task execution flow
return Task( return Task(
config=self.tasks_config['research_task'], config=self.tasks_config['research_task'],
agent=self.researcher(), agent=self.researcher(),
@@ -872,7 +872,7 @@ class ErrorSeverity(Enum):
class CrewError(Exception): class CrewError(Exception):
"""Base exception for CrewAI applications""" """Base exception for CrewAI applications"""
def __init__(self, message: str, severity: ErrorSeverity = ErrorSeverity.MEDIUM, def __init__(self, message: str, severity: ErrorSeverity = ErrorSeverity.MEDIUM,
context: dict = None): context: dict = None):
super().__init__(message) super().__init__(message)
self.severity = severity self.severity = severity
@@ -893,19 +893,19 @@ class ConfigurationError(CrewError):
class ErrorHandler: class ErrorHandler:
"""Centralized error handling for CrewAI applications""" """Centralized error handling for CrewAI applications"""
def __init__(self, crew_name: str): def __init__(self, crew_name: str):
self.crew_name = crew_name self.crew_name = crew_name
self.error_log: List[CrewError] = [] self.error_log: List[CrewError] = []
self.recovery_strategies: Dict[type, Callable] = {} self.recovery_strategies: Dict[type, Callable] = {}
def register_recovery_strategy(self, error_type: type, strategy: Callable): def register_recovery_strategy(self, error_type: type, strategy: Callable):
"""Register a recovery strategy for specific error types""" """Register a recovery strategy for specific error types"""
self.recovery_strategies[error_type] = strategy self.recovery_strategies[error_type] = strategy
def handle_error(self, error: Exception, context: dict = None) -> Any: def handle_error(self, error: Exception, context: dict = None) -> Any:
"""Handle errors with appropriate recovery strategies""" """Handle errors with appropriate recovery strategies"""
# Convert to CrewError if needed # Convert to CrewError if needed
if not isinstance(error, CrewError): if not isinstance(error, CrewError):
crew_error = CrewError( crew_error = CrewError(
@@ -915,11 +915,11 @@ class ErrorHandler:
) )
else: else:
crew_error = error crew_error = error
# Log the error # Log the error
self.error_log.append(crew_error) self.error_log.append(crew_error)
self._log_error(crew_error) self._log_error(crew_error)
# Apply recovery strategy if available # Apply recovery strategy if available
error_type = type(error) error_type = type(error)
if error_type in self.recovery_strategies: if error_type in self.recovery_strategies:
@@ -931,21 +931,21 @@ class ErrorHandler:
ErrorSeverity.HIGH, ErrorSeverity.HIGH,
{"original_error": str(error), "recovery_error": str(recovery_error)} {"original_error": str(error), "recovery_error": str(recovery_error)}
)) ))
# If critical, re-raise # If critical, re-raise
if crew_error.severity == ErrorSeverity.CRITICAL: if crew_error.severity == ErrorSeverity.CRITICAL:
raise crew_error raise crew_error
return None return None
def _log_error(self, error: CrewError): def _log_error(self, error: CrewError):
"""Log error with appropriate level based on severity""" """Log error with appropriate level based on severity"""
logger = logging.getLogger(f"CrewAI.{self.crew_name}.ErrorHandler") logger = logging.getLogger(f"CrewAI.{self.crew_name}.ErrorHandler")
error_msg = f"[{error.severity.value.upper()}] {error}" error_msg = f"[{error.severity.value.upper()}] {error}"
if error.context: if error.context:
error_msg += f" | Context: {error.context}" error_msg += f" | Context: {error.context}"
if error.severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]: if error.severity in [ErrorSeverity.HIGH, ErrorSeverity.CRITICAL]:
logger.error(error_msg) logger.error(error_msg)
logger.error(f"Stack trace: {traceback.format_exc()}") logger.error(f"Stack trace: {traceback.format_exc()}")
@@ -953,16 +953,16 @@ class ErrorHandler:
logger.warning(error_msg) logger.warning(error_msg)
else: else:
logger.info(error_msg) logger.info(error_msg)
def get_error_summary(self) -> Dict[str, Any]: def get_error_summary(self) -> Dict[str, Any]:
"""Get summary of errors encountered""" """Get summary of errors encountered"""
if not self.error_log: if not self.error_log:
return {"total_errors": 0} return {"total_errors": 0}
severity_counts = {} severity_counts = {}
for error in self.error_log: for error in self.error_log:
severity_counts[error.severity.value] = severity_counts.get(error.severity.value, 0) + 1 severity_counts[error.severity.value] = severity_counts.get(error.severity.value, 0) + 1
return { return {
"total_errors": len(self.error_log), "total_errors": len(self.error_log),
"severity_breakdown": severity_counts, "severity_breakdown": severity_counts,
@@ -1004,7 +1004,7 @@ def robust_task(self) -> Task:
# Use fallback response # Use fallback response
return "Task failed, using fallback response" return "Task failed, using fallback response"
return wrapper return wrapper
return Task( return Task(
config=self.tasks_config['research_task'], config=self.tasks_config['research_task'],
agent=self.researcher() agent=self.researcher()
@@ -1020,60 +1020,60 @@ from pydantic import BaseSettings, Field, validator
class Environment(str, Enum): class Environment(str, Enum):
DEVELOPMENT = "development" DEVELOPMENT = "development"
TESTING = "testing" TESTING = "testing"
STAGING = "staging" STAGING = "staging"
PRODUCTION = "production" PRODUCTION = "production"
class CrewAISettings(BaseSettings): class CrewAISettings(BaseSettings):
"""Comprehensive settings management for CrewAI applications""" """Comprehensive settings management for CrewAI applications"""
# Environment # Environment
environment: Environment = Field(default=Environment.DEVELOPMENT) environment: Environment = Field(default=Environment.DEVELOPMENT)
debug: bool = Field(default=True) debug: bool = Field(default=True)
# API Keys (loaded from environment) # API Keys (loaded from environment)
openai_api_key: Optional[str] = Field(default=None, env="OPENAI_API_KEY") openai_api_key: Optional[str] = Field(default=None, env="OPENAI_API_KEY")
anthropic_api_key: Optional[str] = Field(default=None, env="ANTHROPIC_API_KEY") anthropic_api_key: Optional[str] = Field(default=None, env="ANTHROPIC_API_KEY")
serper_api_key: Optional[str] = Field(default=None, env="SERPER_API_KEY") serper_api_key: Optional[str] = Field(default=None, env="SERPER_API_KEY")
mem0_api_key: Optional[str] = Field(default=None, env="MEM0_API_KEY") mem0_api_key: Optional[str] = Field(default=None, env="MEM0_API_KEY")
# CrewAI Configuration # CrewAI Configuration
crew_max_rpm: int = Field(default=100) crew_max_rpm: int = Field(default=100)
crew_max_execution_time: int = Field(default=3600) # 1 hour crew_max_execution_time: int = Field(default=3600) # 1 hour
default_llm_model: str = Field(default="gpt-4") default_llm_model: str = Field(default="gpt-4")
fallback_llm_model: str = Field(default="gpt-3.5-turbo") fallback_llm_model: str = Field(default="gpt-3.5-turbo")
# Memory and Storage # Memory and Storage
crewai_storage_dir: str = Field(default="./storage", env="CREWAI_STORAGE_DIR") crewai_storage_dir: str = Field(default="./storage", env="CREWAI_STORAGE_DIR")
memory_enabled: bool = Field(default=True) memory_enabled: bool = Field(default=True)
memory_cleanup_interval: int = Field(default=86400) # 24 hours in seconds memory_cleanup_interval: int = Field(default=86400) # 24 hours in seconds
# Performance # Performance
enable_caching: bool = Field(default=True) enable_caching: bool = Field(default=True)
max_retries: int = Field(default=3) max_retries: int = Field(default=3)
retry_delay: float = Field(default=1.0) retry_delay: float = Field(default=1.0)
# Monitoring # Monitoring
enable_monitoring: bool = Field(default=True) enable_monitoring: bool = Field(default=True)
log_level: str = Field(default="INFO") log_level: str = Field(default="INFO")
metrics_export_interval: int = Field(default=3600) # 1 hour metrics_export_interval: int = Field(default=3600) # 1 hour
# Security # Security
input_sanitization: bool = Field(default=True) input_sanitization: bool = Field(default=True)
max_input_length: int = Field(default=10000) max_input_length: int = Field(default=10000)
allowed_file_types: list = Field(default=["txt", "md", "pdf", "docx"]) allowed_file_types: list = Field(default=["txt", "md", "pdf", "docx"])
@validator('environment', pre=True) @validator('environment', pre=True)
def set_debug_based_on_env(cls, v): def set_debug_based_on_env(cls, v):
return v return v
@validator('debug') @validator('debug')
def set_debug_from_env(cls, v, values): def set_debug_from_env(cls, v, values):
env = values.get('environment') env = values.get('environment')
if env == Environment.PRODUCTION: if env == Environment.PRODUCTION:
return False return False
return v return v
@validator('openai_api_key') @validator('openai_api_key')
def validate_openai_key(cls, v): def validate_openai_key(cls, v):
if not v: if not v:
@@ -1081,15 +1081,15 @@ class CrewAISettings(BaseSettings):
if not v.startswith('sk-'): if not v.startswith('sk-'):
raise ValueError("Invalid OpenAI API key format") raise ValueError("Invalid OpenAI API key format")
return v return v
@property @property
def is_production(self) -> bool: def is_production(self) -> bool:
return self.environment == Environment.PRODUCTION return self.environment == Environment.PRODUCTION
@property @property
def is_development(self) -> bool: def is_development(self) -> bool:
return self.environment == Environment.DEVELOPMENT return self.environment == Environment.DEVELOPMENT
def get_llm_config(self) -> Dict[str, Any]: def get_llm_config(self) -> Dict[str, Any]:
"""Get LLM configuration based on environment""" """Get LLM configuration based on environment"""
config = { config = {
@@ -1098,12 +1098,12 @@ class CrewAISettings(BaseSettings):
"max_tokens": 4000 if self.is_production else 2000, "max_tokens": 4000 if self.is_production else 2000,
"timeout": 60 "timeout": 60
} }
if self.is_development: if self.is_development:
config["model"] = self.fallback_llm_model config["model"] = self.fallback_llm_model
return config return config
def get_memory_config(self) -> Dict[str, Any]: def get_memory_config(self) -> Dict[str, Any]:
"""Get memory configuration""" """Get memory configuration"""
return { return {
@@ -1112,7 +1112,7 @@ class CrewAISettings(BaseSettings):
"cleanup_interval": self.memory_cleanup_interval, "cleanup_interval": self.memory_cleanup_interval,
"provider": "mem0" if self.mem0_api_key and self.is_production else "local" "provider": "mem0" if self.mem0_api_key and self.is_production else "local"
} }
class Config: class Config:
env_file = ".env" env_file = ".env"
env_file_encoding = 'utf-8' env_file_encoding = 'utf-8'
@@ -1125,25 +1125,25 @@ settings = CrewAISettings()
@CrewBase @CrewBase
class ConfigurableCrew(): class ConfigurableCrew():
"""Crew that uses centralized configuration""" """Crew that uses centralized configuration"""
def __init__(self): def __init__(self):
self.settings = settings self.settings = settings
self.validate_configuration() self.validate_configuration()
def validate_configuration(self): def validate_configuration(self):
"""Validate configuration before crew execution""" """Validate configuration before crew execution"""
required_keys = [self.settings.openai_api_key] required_keys = [self.settings.openai_api_key]
if not all(required_keys): if not all(required_keys):
raise ConfigurationError("Missing required API keys") raise ConfigurationError("Missing required API keys")
if not os.path.exists(self.settings.crewai_storage_dir): if not os.path.exists(self.settings.crewai_storage_dir):
os.makedirs(self.settings.crewai_storage_dir, exist_ok=True) os.makedirs(self.settings.crewai_storage_dir, exist_ok=True)
@agent @agent
def adaptive_agent(self) -> Agent: def adaptive_agent(self) -> Agent:
"""Agent that adapts to configuration""" """Agent that adapts to configuration"""
llm_config = self.settings.get_llm_config() llm_config = self.settings.get_llm_config()
return Agent( return Agent(
config=self.agents_config['researcher'], config=self.agents_config['researcher'],
llm=llm_config["model"], llm=llm_config["model"],
@@ -1163,7 +1163,7 @@ from crewai.tasks.task_output import TaskOutput
class CrewAITestFramework: class CrewAITestFramework:
"""Comprehensive testing framework for CrewAI applications""" """Comprehensive testing framework for CrewAI applications"""
@staticmethod @staticmethod
def create_mock_agent(role: str = "test_agent", tools: list = None) -> Mock: def create_mock_agent(role: str = "test_agent", tools: list = None) -> Mock:
"""Create a mock agent for testing""" """Create a mock agent for testing"""
@@ -1175,9 +1175,9 @@ class CrewAITestFramework:
mock_agent.llm = "gpt-3.5-turbo" mock_agent.llm = "gpt-3.5-turbo"
mock_agent.verbose = False mock_agent.verbose = False
return mock_agent return mock_agent
@staticmethod @staticmethod
def create_mock_task_output(content: str, success: bool = True, def create_mock_task_output(content: str, success: bool = True,
tokens: int = 100) -> TaskOutput: tokens: int = 100) -> TaskOutput:
"""Create a mock task output for testing""" """Create a mock task output for testing"""
return TaskOutput( return TaskOutput(
@@ -1187,13 +1187,13 @@ class CrewAITestFramework:
pydantic=None, pydantic=None,
json_dict=None json_dict=None
) )
@staticmethod @staticmethod
def create_test_crew(agents: list = None, tasks: list = None) -> Crew: def create_test_crew(agents: list = None, tasks: list = None) -> Crew:
"""Create a test crew with mock components""" """Create a test crew with mock components"""
test_agents = agents or [CrewAITestFramework.create_mock_agent()] test_agents = agents or [CrewAITestFramework.create_mock_agent()]
test_tasks = tasks or [] test_tasks = tasks or []
return Crew( return Crew(
agents=test_agents, agents=test_agents,
tasks=test_tasks, tasks=test_tasks,
@@ -1203,53 +1203,53 @@ class CrewAITestFramework:
# Example test cases # Example test cases
class TestResearchCrew: class TestResearchCrew:
"""Test cases for research crew functionality""" """Test cases for research crew functionality"""
def setup_method(self): def setup_method(self):
"""Setup test environment""" """Setup test environment"""
self.framework = CrewAITestFramework() self.framework = CrewAITestFramework()
self.mock_serper = Mock() self.mock_serper = Mock()
@patch('crewai_tools.SerperDevTool') @patch('crewai_tools.SerperDevTool')
def test_agent_creation(self, mock_serper_tool): def test_agent_creation(self, mock_serper_tool):
"""Test agent creation with proper configuration""" """Test agent creation with proper configuration"""
mock_serper_tool.return_value = self.mock_serper mock_serper_tool.return_value = self.mock_serper
crew = ResearchCrew() crew = ResearchCrew()
researcher = crew.researcher() researcher = crew.researcher()
assert researcher.role == "Senior Research Analyst" assert researcher.role == "Senior Research Analyst"
assert len(researcher.tools) > 0 assert len(researcher.tools) > 0
assert researcher.verbose is True assert researcher.verbose is True
def test_task_validation(self): def test_task_validation(self):
"""Test task validation logic""" """Test task validation logic"""
crew = ResearchCrew() crew = ResearchCrew()
# Test valid output # Test valid output
valid_output = self.framework.create_mock_task_output( valid_output = self.framework.create_mock_task_output(
"This is a comprehensive research summary with conclusions and findings." "This is a comprehensive research summary with conclusions and findings."
) )
is_valid, message = crew.validate_research_quality(valid_output) is_valid, message = crew.validate_research_quality(valid_output)
assert is_valid is True assert is_valid is True
# Test invalid output (too short) # Test invalid output (too short)
invalid_output = self.framework.create_mock_task_output("Too short") invalid_output = self.framework.create_mock_task_output("Too short")
is_valid, message = crew.validate_research_quality(invalid_output) is_valid, message = crew.validate_research_quality(invalid_output)
assert is_valid is False assert is_valid is False
assert "brief" in message.lower() assert "brief" in message.lower()
@patch('requests.get') @patch('requests.get')
def test_tool_error_handling(self, mock_requests): def test_tool_error_handling(self, mock_requests):
"""Test tool error handling and recovery""" """Test tool error handling and recovery"""
# Simulate network error # Simulate network error
mock_requests.side_effect = requests.exceptions.RequestException("Network error") mock_requests.side_effect = requests.exceptions.RequestException("Network error")
tool = RobustSearchTool() tool = RobustSearchTool()
result = tool._run("test query") result = tool._run("test query")
assert "network error" in result.lower() assert "network error" in result.lower()
assert "failed" in result.lower() assert "failed" in result.lower()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_crew_execution_flow(self): async def test_crew_execution_flow(self):
"""Test complete crew execution with mocked dependencies""" """Test complete crew execution with mocked dependencies"""
@@ -1257,18 +1257,18 @@ class TestResearchCrew:
mock_execute.return_value = self.framework.create_mock_task_output( mock_execute.return_value = self.framework.create_mock_task_output(
"Research completed successfully with findings and recommendations." "Research completed successfully with findings and recommendations."
) )
crew = ResearchCrew() crew = ResearchCrew()
result = crew.crew().kickoff(inputs={"topic": "AI testing"}) result = crew.crew().kickoff(inputs={"topic": "AI testing"})
assert result is not None assert result is not None
assert "successfully" in result.raw.lower() assert "successfully" in result.raw.lower()
def test_memory_integration(self): def test_memory_integration(self):
"""Test memory system integration""" """Test memory system integration"""
crew = ResearchCrew() crew = ResearchCrew()
memory_manager = AdvancedMemoryManager(crew) memory_manager = AdvancedMemoryManager(crew)
# Test saving to memory # Test saving to memory
test_content = "Important research finding about AI" test_content = "Important research finding about AI"
memory_manager.save_with_context( memory_manager.save_with_context(
@@ -1277,34 +1277,34 @@ class TestResearchCrew:
metadata={"importance": "high"}, metadata={"importance": "high"},
agent="researcher" agent="researcher"
) )
# Test searching memory # Test searching memory
results = memory_manager.search_across_memories("AI research") results = memory_manager.search_across_memories("AI research")
assert "short_term" in results assert "short_term" in results
def test_error_handling_workflow(self): def test_error_handling_workflow(self):
"""Test error handling and recovery mechanisms""" """Test error handling and recovery mechanisms"""
error_handler = ErrorHandler("test_crew") error_handler = ErrorHandler("test_crew")
# Test error registration and handling # Test error registration and handling
test_error = TaskExecutionError("Test task failed", ErrorSeverity.MEDIUM) test_error = TaskExecutionError("Test task failed", ErrorSeverity.MEDIUM)
result = error_handler.handle_error(test_error) result = error_handler.handle_error(test_error)
assert len(error_handler.error_log) == 1 assert len(error_handler.error_log) == 1
assert error_handler.error_log[0].severity == ErrorSeverity.MEDIUM assert error_handler.error_log[0].severity == ErrorSeverity.MEDIUM
def test_configuration_validation(self): def test_configuration_validation(self):
"""Test configuration validation""" """Test configuration validation"""
# Test with missing API key # Test with missing API key
with patch.dict(os.environ, {}, clear=True): with patch.dict(os.environ, {}, clear=True):
with pytest.raises(ValueError): with pytest.raises(ValueError):
settings = CrewAISettings() settings = CrewAISettings()
# Test with valid configuration # Test with valid configuration
with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test-key"}): with patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test-key"}):
settings = CrewAISettings() settings = CrewAISettings()
assert settings.openai_api_key == "sk-test-key" assert settings.openai_api_key == "sk-test-key"
@pytest.mark.integration @pytest.mark.integration
def test_end_to_end_workflow(self): def test_end_to_end_workflow(self):
"""Integration test for complete workflow""" """Integration test for complete workflow"""
@@ -1315,41 +1315,41 @@ class TestResearchCrew:
# Performance testing # Performance testing
class TestCrewPerformance: class TestCrewPerformance:
"""Performance tests for CrewAI applications""" """Performance tests for CrewAI applications"""
def test_memory_usage(self): def test_memory_usage(self):
"""Test memory usage during crew execution""" """Test memory usage during crew execution"""
import psutil import psutil
import gc import gc
process = psutil.Process() process = psutil.Process()
initial_memory = process.memory_info().rss initial_memory = process.memory_info().rss
# Create and run crew multiple times # Create and run crew multiple times
for i in range(10): for i in range(10):
crew = ResearchCrew() crew = ResearchCrew()
# Simulate crew execution # Simulate crew execution
del crew del crew
gc.collect() gc.collect()
final_memory = process.memory_info().rss final_memory = process.memory_info().rss
memory_increase = final_memory - initial_memory memory_increase = final_memory - initial_memory
# Assert memory increase is reasonable (less than 100MB) # Assert memory increase is reasonable (less than 100MB)
assert memory_increase < 100 * 1024 * 1024 assert memory_increase < 100 * 1024 * 1024
def test_concurrent_execution(self): def test_concurrent_execution(self):
"""Test concurrent crew execution""" """Test concurrent crew execution"""
import concurrent.futures import concurrent.futures
def run_crew(crew_id): def run_crew(crew_id):
crew = ResearchCrew() crew = ResearchCrew()
# Simulate execution # Simulate execution
return f"crew_{crew_id}_completed" return f"crew_{crew_id}_completed"
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(run_crew, i) for i in range(5)] futures = [executor.submit(run_crew, i) for i in range(5)]
results = [future.result() for future in futures] results = [future.result() for future in futures]
assert len(results) == 5 assert len(results) == 5
assert all("completed" in result for result in results) assert all("completed" in result for result in results)
@@ -1400,7 +1400,7 @@ class TestCrewPerformance:
### Development: ### Development:
1. Always use .env files for sensitive configuration 1. Always use .env files for sensitive configuration
2. Implement comprehensive error handling and logging 2. Implement comprehensive error handling and logging
3. Use structured outputs with Pydantic for reliability 3. Use structured outputs with Pydantic for reliability
4. Test crew functionality with different input scenarios 4. Test crew functionality with different input scenarios
5. Follow CrewAI patterns and conventions consistently 5. Follow CrewAI patterns and conventions consistently
@@ -1426,4 +1426,4 @@ class TestCrewPerformance:
5. Use async patterns for I/O-bound operations 5. Use async patterns for I/O-bound operations
6. Implement proper connection pooling and resource management 6. Implement proper connection pooling and resource management
7. Profile and optimize critical paths 7. Profile and optimize critical paths
8. Plan for horizontal scaling when needed 8. Plan for horizontal scaling when needed

View File

@@ -33,7 +33,7 @@ dependencies = [
"click>=8.1.7", "click>=8.1.7",
"appdirs>=1.4.4", "appdirs>=1.4.4",
"jsonref>=1.1.0", "jsonref>=1.1.0",
"json-repair>=0.25.2", "json-repair==0.25.2",
"uv>=0.4.25", "uv>=0.4.25",
"tomli-w>=1.1.0", "tomli-w>=1.1.0",
"tomli>=2.0.2", "tomli>=2.0.2",
@@ -47,11 +47,11 @@ Documentation = "https://docs.crewai.com"
Repository = "https://github.com/crewAIInc/crewAI" Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies] [project.optional-dependencies]
tools = ["crewai-tools~=0.48.0"] tools = ["crewai-tools~=0.49.0"]
embeddings = [ embeddings = [
"tiktoken~=0.8.0" "tiktoken~=0.8.0"
] ]
agentops = ["agentops>=0.3.0"] agentops = ["agentops==0.3.18"]
pdfplumber = [ pdfplumber = [
"pdfplumber>=0.11.4", "pdfplumber>=0.11.4",
] ]
@@ -123,3 +123,15 @@ path = "src/crewai/__init__.py"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
exclude = [
"docs/**",
"docs/",
]
[tool.hatch.build.targets.sdist]
exclude = [
"docs/**",
"docs/",
]

View File

@@ -28,19 +28,19 @@ _telemetry_submitted = False
def _track_install(): def _track_install():
"""Track package installation/first-use via Scarf analytics.""" """Track package installation/first-use via Scarf analytics."""
global _telemetry_submitted global _telemetry_submitted
if _telemetry_submitted or Telemetry._is_telemetry_disabled(): if _telemetry_submitted or Telemetry._is_telemetry_disabled():
return return
try: try:
pixel_url = "https://api.scarf.sh/v2/packages/CrewAI/crewai/docs/00f2dad1-8334-4a39-934e-003b2e1146db" pixel_url = "https://api.scarf.sh/v2/packages/CrewAI/crewai/docs/00f2dad1-8334-4a39-934e-003b2e1146db"
req = urllib.request.Request(pixel_url) req = urllib.request.Request(pixel_url)
req.add_header('User-Agent', f'CrewAI-Python/{__version__}') req.add_header('User-Agent', f'CrewAI-Python/{__version__}')
with urllib.request.urlopen(req, timeout=2): # nosec B310 with urllib.request.urlopen(req, timeout=2): # nosec B310
_telemetry_submitted = True _telemetry_submitted = True
except Exception: except Exception:
pass pass
@@ -54,7 +54,7 @@ def _track_install_async():
_track_install_async() _track_install_async()
__version__ = "0.134.0" __version__ = "0.140.0"
__all__ = [ __all__ = [
"Agent", "Agent",
"Crew", "Crew",

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }] authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14" requires-python = ">=3.10,<3.14"
dependencies = [ dependencies = [
"crewai[tools]>=0.134.0,<1.0.0" "crewai[tools]>=0.140.0,<1.0.0"
] ]
[project.scripts] [project.scripts]

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }] authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14" requires-python = ">=3.10,<3.14"
dependencies = [ dependencies = [
"crewai[tools]>=0.134.0,<1.0.0", "crewai[tools]>=0.140.0,<1.0.0",
] ]
[project.scripts] [project.scripts]

View File

@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10,<3.14" requires-python = ">=3.10,<3.14"
dependencies = [ dependencies = [
"crewai[tools]>=0.134.0" "crewai[tools]>=0.140.0"
] ]
[tool.crewai] [tool.crewai]

File diff suppressed because one or more lines are too long

View File

@@ -52,6 +52,7 @@ from crewai.utilities.events.memory_events import (
MemoryRetrievalCompletedEvent, MemoryRetrievalCompletedEvent,
) )
@pytest.fixture @pytest.fixture
def ceo(): def ceo():
return Agent( return Agent(
@@ -935,12 +936,27 @@ def test_cache_hitting_between_agents(researcher, writer, ceo):
read.return_value = "12" read.return_value = "12"
crew.kickoff() crew.kickoff()
assert read.call_count == 2, "read was not called exactly twice" assert read.call_count == 2, "read was not called exactly twice"
# Check if read was called with the expected arguments
expected_calls = [ # Filter the mock calls to only include the ones with 'tool' and 'input' keywords
call(tool="multiplier", input={"first_number": 2, "second_number": 6}), cache_calls = [
call(tool="multiplier", input={"first_number": 2, "second_number": 6}), call
for call in read.call_args_list
if len(call.kwargs) == 2
and "tool" in call.kwargs
and "input" in call.kwargs
] ]
read.assert_has_calls(expected_calls, any_order=False)
# Check if we have the expected number of cache calls
assert len(cache_calls) == 2, f"Expected 2 cache calls, got {len(cache_calls)}"
# Check if both calls were made with the expected arguments
expected_call = call(
tool="multiplier", input={"first_number": 2, "second_number": 6}
)
assert cache_calls[0] == expected_call, f"First call mismatch: {cache_calls[0]}"
assert (
cache_calls[1] == expected_call
), f"Second call mismatch: {cache_calls[1]}"
@pytest.mark.vcr(filter_headers=["authorization"]) @pytest.mark.vcr(filter_headers=["authorization"])
@@ -1797,7 +1813,7 @@ def test_hierarchical_kickoff_usage_metrics_include_manager(researcher):
agent=researcher, # *regular* agent agent=researcher, # *regular* agent
) )
# ── 2. Stub out each agents _token_process.get_summary() ─────────────────── # ── 2. Stub out each agent's _token_process.get_summary() ───────────────────
researcher_metrics = UsageMetrics( researcher_metrics = UsageMetrics(
total_tokens=120, prompt_tokens=80, completion_tokens=40, successful_requests=2 total_tokens=120, prompt_tokens=80, completion_tokens=40, successful_requests=2
) )
@@ -1821,7 +1837,7 @@ def test_hierarchical_kickoff_usage_metrics_include_manager(researcher):
process=Process.hierarchical, process=Process.hierarchical,
) )
# We dont care about LLM output here; patch execute_sync to avoid network # We don't care about LLM output here; patch execute_sync to avoid network
with patch.object( with patch.object(
Task, Task,
"execute_sync", "execute_sync",
@@ -2489,17 +2505,19 @@ def test_using_contextual_memory():
memory=True, memory=True,
) )
with patch.object(ContextualMemory, "build_context_for_task", return_value="") as contextual_mem: with patch.object(
ContextualMemory, "build_context_for_task", return_value=""
) as contextual_mem:
crew.kickoff() crew.kickoff()
contextual_mem.assert_called_once() contextual_mem.assert_called_once()
@pytest.mark.vcr(filter_headers=["authorization"]) @pytest.mark.vcr(filter_headers=["authorization"])
def test_memory_events_are_emitted(): def test_memory_events_are_emitted():
events = defaultdict(list) events = defaultdict(list)
with crewai_event_bus.scoped_handlers(): with crewai_event_bus.scoped_handlers():
@crewai_event_bus.on(MemorySaveStartedEvent) @crewai_event_bus.on(MemorySaveStartedEvent)
def handle_memory_save_started(source, event): def handle_memory_save_started(source, event):
events["MemorySaveStartedEvent"].append(event) events["MemorySaveStartedEvent"].append(event)
@@ -2562,6 +2580,7 @@ def test_memory_events_are_emitted():
assert len(events["MemoryRetrievalStartedEvent"]) == 1 assert len(events["MemoryRetrievalStartedEvent"]) == 1
assert len(events["MemoryRetrievalCompletedEvent"]) == 1 assert len(events["MemoryRetrievalCompletedEvent"]) == 1
@pytest.mark.vcr(filter_headers=["authorization"]) @pytest.mark.vcr(filter_headers=["authorization"])
def test_using_contextual_memory_with_long_term_memory(): def test_using_contextual_memory_with_long_term_memory():
from unittest.mock import patch from unittest.mock import patch
@@ -2585,7 +2604,9 @@ def test_using_contextual_memory_with_long_term_memory():
long_term_memory=LongTermMemory(), long_term_memory=LongTermMemory(),
) )
with patch.object(ContextualMemory, "build_context_for_task", return_value="") as contextual_mem: with patch.object(
ContextualMemory, "build_context_for_task", return_value=""
) as contextual_mem:
crew.kickoff() crew.kickoff()
contextual_mem.assert_called_once() contextual_mem.assert_called_once()
assert crew.memory is False assert crew.memory is False
@@ -2686,7 +2707,9 @@ def test_using_contextual_memory_with_short_term_memory():
short_term_memory=ShortTermMemory(), short_term_memory=ShortTermMemory(),
) )
with patch.object(ContextualMemory, "build_context_for_task", return_value="") as contextual_mem: with patch.object(
ContextualMemory, "build_context_for_task", return_value=""
) as contextual_mem:
crew.kickoff() crew.kickoff()
contextual_mem.assert_called_once() contextual_mem.assert_called_once()
assert crew.memory is False assert crew.memory is False
@@ -2715,7 +2738,9 @@ def test_disabled_memory_using_contextual_memory():
memory=False, memory=False,
) )
with patch.object(ContextualMemory, "build_context_for_task", return_value="") as contextual_mem: with patch.object(
ContextualMemory, "build_context_for_task", return_value=""
) as contextual_mem:
crew.kickoff() crew.kickoff()
contextual_mem.assert_not_called() contextual_mem.assert_not_called()

5400
uv.lock generated

File diff suppressed because it is too large Load Diff