diff --git a/src/crewai/agent.py b/src/crewai/agent.py index f07408133..cfebc18e5 100644 --- a/src/crewai/agent.py +++ b/src/crewai/agent.py @@ -114,7 +114,6 @@ class Agent(BaseAgent): @model_validator(mode="after") def post_init_setup(self): - self._set_knowledge() self.agent_ops_agent_name = self.role self.llm = create_llm(self.llm) @@ -134,8 +133,11 @@ class Agent(BaseAgent): self.cache_handler = CacheHandler() self.set_cache_handler(self.cache_handler) - def _set_knowledge(self): + def set_knowledge(self, crew_embedder: Optional[Dict[str, Any]] = None): try: + if self.embedder is None and crew_embedder: + self.embedder = crew_embedder + if self.knowledge_sources: full_pattern = re.compile(r"[^a-zA-Z0-9\-_\r\n]|(\.\.)") knowledge_agent_name = f"{re.sub(full_pattern, '_', self.role)}" diff --git a/src/crewai/agents/agent_builder/base_agent.py b/src/crewai/agents/agent_builder/base_agent.py index 64110c2ae..f39fafb99 100644 --- a/src/crewai/agents/agent_builder/base_agent.py +++ b/src/crewai/agents/agent_builder/base_agent.py @@ -351,3 +351,6 @@ class BaseAgent(ABC, BaseModel): if not self._rpm_controller: self._rpm_controller = rpm_controller self.create_agent_executor() + + def set_knowledge(self, crew_embedder: Optional[Dict[str, Any]] = None): + pass diff --git a/src/crewai/cli/constants.py b/src/crewai/cli/constants.py index b97b4f208..fec0b6384 100644 --- a/src/crewai/cli/constants.py +++ b/src/crewai/cli/constants.py @@ -216,10 +216,43 @@ MODELS = { "watsonx/ibm/granite-3-8b-instruct", ], "bedrock": [ + "bedrock/us.amazon.nova-pro-v1:0", + "bedrock/us.amazon.nova-micro-v1:0", + "bedrock/us.amazon.nova-lite-v1:0", + "bedrock/us.anthropic.claude-3-5-sonnet-20240620-v1:0", + "bedrock/us.anthropic.claude-3-5-haiku-20241022-v1:0", + "bedrock/us.anthropic.claude-3-5-sonnet-20241022-v2:0", + "bedrock/us.anthropic.claude-3-7-sonnet-20250219-v1:0", + "bedrock/us.anthropic.claude-3-sonnet-20240229-v1:0", + "bedrock/us.anthropic.claude-3-opus-20240229-v1:0", + "bedrock/us.anthropic.claude-3-haiku-20240307-v1:0", + "bedrock/us.meta.llama3-2-11b-instruct-v1:0", + "bedrock/us.meta.llama3-2-3b-instruct-v1:0", + "bedrock/us.meta.llama3-2-90b-instruct-v1:0", + "bedrock/us.meta.llama3-2-1b-instruct-v1:0", + "bedrock/us.meta.llama3-1-8b-instruct-v1:0", + "bedrock/us.meta.llama3-1-70b-instruct-v1:0", + "bedrock/us.meta.llama3-3-70b-instruct-v1:0", + "bedrock/us.meta.llama3-1-405b-instruct-v1:0", + "bedrock/eu.anthropic.claude-3-5-sonnet-20240620-v1:0", + "bedrock/eu.anthropic.claude-3-sonnet-20240229-v1:0", + "bedrock/eu.anthropic.claude-3-haiku-20240307-v1:0", + "bedrock/eu.meta.llama3-2-3b-instruct-v1:0", + "bedrock/eu.meta.llama3-2-1b-instruct-v1:0", + "bedrock/apac.anthropic.claude-3-5-sonnet-20240620-v1:0", + "bedrock/apac.anthropic.claude-3-5-sonnet-20241022-v2:0", + "bedrock/apac.anthropic.claude-3-sonnet-20240229-v1:0", + "bedrock/apac.anthropic.claude-3-haiku-20240307-v1:0", + "bedrock/amazon.nova-pro-v1:0", + "bedrock/amazon.nova-micro-v1:0", + "bedrock/amazon.nova-lite-v1:0", "bedrock/anthropic.claude-3-5-sonnet-20240620-v1:0", + "bedrock/anthropic.claude-3-5-haiku-20241022-v1:0", + "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0", + "bedrock/anthropic.claude-3-7-sonnet-20250219-v1:0", "bedrock/anthropic.claude-3-sonnet-20240229-v1:0", - "bedrock/anthropic.claude-3-haiku-20240307-v1:0", "bedrock/anthropic.claude-3-opus-20240229-v1:0", + "bedrock/anthropic.claude-3-haiku-20240307-v1:0", "bedrock/anthropic.claude-v2:1", "bedrock/anthropic.claude-v2", "bedrock/anthropic.claude-instant-v1", @@ -234,8 +267,6 @@ MODELS = { "bedrock/ai21.j2-mid-v1", "bedrock/ai21.j2-ultra-v1", "bedrock/ai21.jamba-instruct-v1:0", - "bedrock/meta.llama2-13b-chat-v1", - "bedrock/meta.llama2-70b-chat-v1", "bedrock/mistral.mistral-7b-instruct-v0:2", "bedrock/mistral.mixtral-8x7b-instruct-v0:1", ], diff --git a/src/crewai/crew.py b/src/crewai/crew.py index cf627700e..9cecfed3a 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -600,6 +600,7 @@ class Crew(BaseModel): agent.i18n = i18n # type: ignore[attr-defined] # Argument 1 to "_interpolate_inputs" of "Crew" has incompatible type "dict[str, Any] | None"; expected "dict[str, Any]" agent.crew = self # type: ignore[attr-defined] + agent.set_knowledge(crew_embedder=self.embedder) # TODO: Create an AgentFunctionCalling protocol for future refactoring if not agent.function_calling_llm: # type: ignore # "BaseAgent" has no attribute "function_calling_llm" agent.function_calling_llm = self.function_calling_llm # type: ignore # "BaseAgent" has no attribute "function_calling_llm" diff --git a/src/crewai/flow/utils.py b/src/crewai/flow/utils.py index c0686222f..81f3c1041 100644 --- a/src/crewai/flow/utils.py +++ b/src/crewai/flow/utils.py @@ -16,7 +16,8 @@ Example import ast import inspect import textwrap -from typing import Any, Dict, List, Optional, Set, Union +from collections import defaultdict, deque +from typing import Any, Deque, Dict, List, Optional, Set, Union def get_possible_return_constants(function: Any) -> Optional[List[str]]: @@ -118,7 +119,7 @@ def calculate_node_levels(flow: Any) -> Dict[str, int]: - Processes router paths separately """ levels: Dict[str, int] = {} - queue: List[str] = [] + queue: Deque[str] = deque() visited: Set[str] = set() pending_and_listeners: Dict[str, Set[str]] = {} @@ -128,28 +129,35 @@ def calculate_node_levels(flow: Any) -> Dict[str, int]: levels[method_name] = 0 queue.append(method_name) + # Precompute listener dependencies + or_listeners = defaultdict(list) + and_listeners = defaultdict(set) + for listener_name, (condition_type, trigger_methods) in flow._listeners.items(): + if condition_type == "OR": + for method in trigger_methods: + or_listeners[method].append(listener_name) + elif condition_type == "AND": + and_listeners[listener_name] = set(trigger_methods) + # Breadth-first traversal to assign levels while queue: - current = queue.pop(0) + current = queue.popleft() current_level = levels[current] visited.add(current) - for listener_name, (condition_type, trigger_methods) in flow._listeners.items(): - if condition_type == "OR": - if current in trigger_methods: - if ( - listener_name not in levels - or levels[listener_name] > current_level + 1 - ): - levels[listener_name] = current_level + 1 - if listener_name not in visited: - queue.append(listener_name) - elif condition_type == "AND": + for listener_name in or_listeners[current]: + if listener_name not in levels or levels[listener_name] > current_level + 1: + levels[listener_name] = current_level + 1 + if listener_name not in visited: + queue.append(listener_name) + + for listener_name, required_methods in and_listeners.items(): + if current in required_methods: if listener_name not in pending_and_listeners: pending_and_listeners[listener_name] = set() - if current in trigger_methods: - pending_and_listeners[listener_name].add(current) - if set(trigger_methods) == pending_and_listeners[listener_name]: + pending_and_listeners[listener_name].add(current) + + if required_methods == pending_and_listeners[listener_name]: if ( listener_name not in levels or levels[listener_name] > current_level + 1 @@ -159,22 +167,7 @@ def calculate_node_levels(flow: Any) -> Dict[str, int]: queue.append(listener_name) # Handle router connections - if current in flow._routers: - router_method_name = current - paths = flow._router_paths.get(router_method_name, []) - for path in paths: - for listener_name, ( - condition_type, - trigger_methods, - ) in flow._listeners.items(): - if path in trigger_methods: - if ( - listener_name not in levels - or levels[listener_name] > current_level + 1 - ): - levels[listener_name] = current_level + 1 - if listener_name not in visited: - queue.append(listener_name) + process_router_paths(flow, current, current_level, levels, queue) return levels @@ -227,10 +220,7 @@ def build_ancestor_dict(flow: Any) -> Dict[str, Set[str]]: def dfs_ancestors( - node: str, - ancestors: Dict[str, Set[str]], - visited: Set[str], - flow: Any + node: str, ancestors: Dict[str, Set[str]], visited: Set[str], flow: Any ) -> None: """ Perform depth-first search to build ancestor relationships. @@ -274,7 +264,9 @@ def dfs_ancestors( dfs_ancestors(listener_name, ancestors, visited, flow) -def is_ancestor(node: str, ancestor_candidate: str, ancestors: Dict[str, Set[str]]) -> bool: +def is_ancestor( + node: str, ancestor_candidate: str, ancestors: Dict[str, Set[str]] +) -> bool: """ Check if one node is an ancestor of another. @@ -339,7 +331,9 @@ def build_parent_children_dict(flow: Any) -> Dict[str, List[str]]: return parent_children -def get_child_index(parent: str, child: str, parent_children: Dict[str, List[str]]) -> int: +def get_child_index( + parent: str, child: str, parent_children: Dict[str, List[str]] +) -> int: """ Get the index of a child node in its parent's sorted children list. @@ -360,3 +354,23 @@ def get_child_index(parent: str, child: str, parent_children: Dict[str, List[str children = parent_children.get(parent, []) children.sort() return children.index(child) + + +def process_router_paths(flow, current, current_level, levels, queue): + """ + Handle the router connections for the current node. + """ + if current in flow._routers: + paths = flow._router_paths.get(current, []) + for path in paths: + for listener_name, ( + condition_type, + trigger_methods, + ) in flow._listeners.items(): + if path in trigger_methods: + if ( + listener_name not in levels + or levels[listener_name] > current_level + 1 + ): + levels[listener_name] = current_level + 1 + queue.append(listener_name) diff --git a/src/crewai/llm.py b/src/crewai/llm.py index 2eefa8934..0c8a46214 100644 --- a/src/crewai/llm.py +++ b/src/crewai/llm.py @@ -64,6 +64,7 @@ LLM_CONTEXT_WINDOW_SIZES = { "gpt-4-turbo": 128000, "o1-preview": 128000, "o1-mini": 128000, + "o3-mini": 200000, # Based on official o3-mini specifications # gemini "gemini-2.0-flash": 1048576, "gemini-1.5-pro": 2097152, @@ -485,10 +486,23 @@ class LLM: """ Returns the context window size, using 75% of the maximum to avoid cutting off messages mid-thread. + + Raises: + ValueError: If a model's context window size is outside valid bounds (1024-2097152) """ if self.context_window_size != 0: return self.context_window_size + MIN_CONTEXT = 1024 + MAX_CONTEXT = 2097152 # Current max from gemini-1.5-pro + + # Validate all context window sizes + for key, value in LLM_CONTEXT_WINDOW_SIZES.items(): + if value < MIN_CONTEXT or value > MAX_CONTEXT: + raise ValueError( + f"Context window for {key} must be between {MIN_CONTEXT} and {MAX_CONTEXT}" + ) + self.context_window_size = int( DEFAULT_CONTEXT_WINDOW_SIZE * CONTEXT_WINDOW_USAGE_RATIO ) diff --git a/src/crewai/utilities/llm_utils.py b/src/crewai/utilities/llm_utils.py index c774a71fb..4d34d789c 100644 --- a/src/crewai/utilities/llm_utils.py +++ b/src/crewai/utilities/llm_utils.py @@ -44,6 +44,7 @@ def create_llm( # Extract attributes with explicit types model = ( getattr(llm_value, "model_name", None) + or getattr(llm_value, "model", None) or getattr(llm_value, "deployment_name", None) or str(llm_value) ) diff --git a/tests/llm_test.py b/tests/llm_test.py index 00bb69aa5..61aa1aced 100644 --- a/tests/llm_test.py +++ b/tests/llm_test.py @@ -6,7 +6,7 @@ import pytest from pydantic import BaseModel from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess -from crewai.llm import LLM +from crewai.llm import CONTEXT_WINDOW_USAGE_RATIO, LLM from crewai.utilities.events import crewai_event_bus from crewai.utilities.events.tool_usage_events import ToolExecutionErrorEvent from crewai.utilities.token_counter_callback import TokenCalcHandler @@ -285,6 +285,23 @@ def test_o3_mini_reasoning_effort_medium(): assert isinstance(result, str) assert "Paris" in result +def test_context_window_validation(): + """Test that context window validation works correctly.""" + # Test valid window size + llm = LLM(model="o3-mini") + assert llm.get_context_window_size() == int(200000 * CONTEXT_WINDOW_USAGE_RATIO) + + # Test invalid window size + with pytest.raises(ValueError) as excinfo: + with patch.dict( + "crewai.llm.LLM_CONTEXT_WINDOW_SIZES", + {"test-model": 500}, # Below minimum + clear=True, + ): + llm = LLM(model="test-model") + llm.get_context_window_size() + assert "must be between 1024 and 2097152" in str(excinfo.value) + @pytest.mark.vcr(filter_headers=["authorization"]) @pytest.fixture