mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-09 00:45:16 +00:00
Compare commits
1 Commits
devin/1778
...
rn/enumera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed1b51e4b4 |
@@ -4,29 +4,6 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="7 مايو 2026">
|
||||
## v1.14.5a3
|
||||
|
||||
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a3)
|
||||
|
||||
## ما الذي تغير
|
||||
|
||||
### إصلاحات الأخطاء
|
||||
- إصلاح مسار نقطة النهاية للحالة من /{kickoff_id}/status إلى /status/{kickoff_id}
|
||||
- تحديث تبعية gitpython إلى الإصدار >=3.1.47 للامتثال الأمني
|
||||
|
||||
### إعادة هيكلة
|
||||
- استخراج واجهة سطر الأوامر إلى حزمة crewai-cli المستقلة
|
||||
|
||||
### الوثائق
|
||||
- تحديث سجل التغييرات والإصدار للإصدار v1.14.5a2
|
||||
|
||||
## المساهمون
|
||||
|
||||
@greysonlalonde, @iris-clawd
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="4 مايو 2026">
|
||||
## v1.14.5a2
|
||||
|
||||
|
||||
@@ -4,29 +4,6 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="May 07, 2026">
|
||||
## v1.14.5a3
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a3)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Bug Fixes
|
||||
- Fix status endpoint path from /{kickoff_id}/status to /status/{kickoff_id}
|
||||
- Bump gitpython dependency to version >=3.1.47 for security compliance
|
||||
|
||||
### Refactoring
|
||||
- Extract CLI into standalone crewai-cli package
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.14.5a2
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde, @iris-clawd
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="May 04, 2026">
|
||||
## v1.14.5a2
|
||||
|
||||
|
||||
@@ -4,29 +4,6 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 5월 7일">
|
||||
## v1.14.5a3
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a3)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 버그 수정
|
||||
- 상태 엔드포인트 경로를 /{kickoff_id}/status에서 /status/{kickoff_id}로 수정
|
||||
- 보안 준수를 위해 gitpython 의존성을 버전 >=3.1.47로 업데이트
|
||||
|
||||
### 리팩토링
|
||||
- CLI를 독립형 crewai-cli 패키지로 분리
|
||||
|
||||
### 문서
|
||||
- v1.14.5a2에 대한 변경 로그 및 버전 업데이트
|
||||
|
||||
## 기여자
|
||||
|
||||
@greysonlalonde, @iris-clawd
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 5월 4일">
|
||||
## v1.14.5a2
|
||||
|
||||
|
||||
@@ -4,29 +4,6 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="07 mai 2026">
|
||||
## v1.14.5a3
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.5a3)
|
||||
|
||||
## O que Mudou
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir o caminho do endpoint de status de /{kickoff_id}/status para /status/{kickoff_id}
|
||||
- Atualizar a dependência gitpython para a versão >=3.1.47 para conformidade de segurança
|
||||
|
||||
### Refatoração
|
||||
- Extrair CLI para o pacote independente crewai-cli
|
||||
|
||||
### Documentação
|
||||
- Atualizar o changelog e a versão para v1.14.5a2
|
||||
|
||||
## Contributors
|
||||
|
||||
@greysonlalonde, @iris-clawd
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="04 mai 2026">
|
||||
## v1.14.5a2
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"crewai-core==1.14.5a3",
|
||||
"crewai-core>=1.14.5a2",
|
||||
"click~=8.1.7",
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"pydantic-settings~=2.10.1",
|
||||
@@ -22,8 +22,6 @@ dependencies = [
|
||||
"packaging>=23.0",
|
||||
"python-dotenv>=1.2.2,<2",
|
||||
"uv~=0.11.6",
|
||||
"textual>=7.5.0",
|
||||
"certifi",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.14.5a3"
|
||||
__version__ = "1.14.5a2"
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "1.14.5a3"
|
||||
__version__ = "1.14.5a2"
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.14.5a3"
|
||||
__version__ = "1.14.5a2"
|
||||
|
||||
@@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests>=2.33.0,<3",
|
||||
"crewai==1.14.5a3",
|
||||
"crewai==1.14.5a2",
|
||||
"tiktoken>=0.8.0,<0.13",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -330,4 +330,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.14.5a3"
|
||||
__version__ = "1.14.5a2"
|
||||
|
||||
@@ -8,8 +8,8 @@ authors = [
|
||||
]
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"crewai-core==1.14.5a3",
|
||||
"crewai-cli==1.14.5a3",
|
||||
"crewai-core==1.14.5a2",
|
||||
"crewai-cli==1.14.5a2",
|
||||
# Core Dependencies
|
||||
"pydantic>=2.11.9,<2.13",
|
||||
"openai>=2.30.0,<3",
|
||||
@@ -28,6 +28,8 @@ dependencies = [
|
||||
# Authentication and Security
|
||||
"python-dotenv>=1.2.2,<2",
|
||||
"pyjwt>=2.9.0,<3",
|
||||
# TUI
|
||||
"textual>=7.5.0",
|
||||
# Configuration and Utils
|
||||
"click~=8.1.7",
|
||||
"appdirs~=1.4.4",
|
||||
@@ -54,7 +56,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.14.5a3",
|
||||
"crewai-tools==1.14.5a2",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken>=0.8.0,<0.13"
|
||||
|
||||
@@ -48,7 +48,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.14.5a3"
|
||||
__version__ = "1.14.5a2"
|
||||
|
||||
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"Memory": ("crewai.memory.unified_memory", "Memory"),
|
||||
|
||||
@@ -512,7 +512,7 @@ class Agent(BaseAgent):
|
||||
The task prompt after memory retrieval, ready for knowledge lookup.
|
||||
"""
|
||||
get_env_context()
|
||||
if not getattr(self.executor_class, "supports_internal_planning", False):
|
||||
if self.executor_class is not AgentExecutor:
|
||||
handle_reasoning(self, task)
|
||||
|
||||
self._inject_date_to_task(task)
|
||||
@@ -1102,40 +1102,6 @@ class Agent(BaseAgent):
|
||||
self.agent_executor.tools_handler = self.tools_handler
|
||||
self.agent_executor.request_within_rpm_limit = rpm_limit_fn
|
||||
|
||||
def _resolve_kickoff_executor_class(self) -> type[AgentExecutor]:
|
||||
"""Resolve the executor class to use for ``Agent.kickoff()``.
|
||||
|
||||
``Agent.kickoff()`` requires a Flow-based executor (one that sets
|
||||
``supports_kickoff = True`` on the class). When the configured
|
||||
:attr:`executor_class` does not support the kickoff flow, this method
|
||||
emits a :class:`DeprecationWarning` and falls back to
|
||||
:class:`~crewai.experimental.agent_executor.AgentExecutor` so that
|
||||
existing default-configured agents continue to work; the warning makes
|
||||
the previously silent override visible to the caller.
|
||||
|
||||
Returns:
|
||||
The :class:`AgentExecutor` subclass to instantiate for kickoff.
|
||||
"""
|
||||
executor_cls = self.executor_class
|
||||
supports_kickoff = getattr(executor_cls, "supports_kickoff", False)
|
||||
if supports_kickoff and isinstance(executor_cls, type):
|
||||
return cast("type[AgentExecutor]", executor_cls)
|
||||
|
||||
warnings.warn(
|
||||
(
|
||||
f"Agent.kickoff() requires a Flow-based AgentExecutor but "
|
||||
f"executor_class={getattr(executor_cls, '__name__', executor_cls)!r} "
|
||||
"does not advertise supports_kickoff=True. Falling back to "
|
||||
"crewai.experimental.agent_executor.AgentExecutor for this "
|
||||
"kickoff() call. Set executor_class=AgentExecutor explicitly "
|
||||
"to silence this warning. The silent fallback will be removed "
|
||||
"in a future release."
|
||||
),
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return AgentExecutor
|
||||
|
||||
def get_delegation_tools(self, agents: Sequence[BaseAgent]) -> list[BaseTool]:
|
||||
agent_tools = AgentTools(agents=agents)
|
||||
return agent_tools.tools()
|
||||
@@ -1433,8 +1399,7 @@ class Agent(BaseAgent):
|
||||
executor.request_within_rpm_limit = rpm_limit_fn
|
||||
executor.callbacks = [TokenCalcHandler(self._token_process)]
|
||||
else:
|
||||
kickoff_executor_cls = self._resolve_kickoff_executor_class()
|
||||
executor = kickoff_executor_cls(
|
||||
executor = AgentExecutor(
|
||||
llm=cast(BaseLLM, self.llm),
|
||||
agent=self,
|
||||
prompt=prompt,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel, Field, PrivateAttr
|
||||
|
||||
@@ -20,25 +20,6 @@ class BaseAgentExecutor(BaseModel):
|
||||
model_config = {"arbitrary_types_allowed": True}
|
||||
|
||||
executor_type: str = "base"
|
||||
|
||||
supports_internal_planning: ClassVar[bool] = False
|
||||
"""Whether this executor implements its own planning/replanning loop.
|
||||
|
||||
When True, ``Agent`` skips the external ``handle_reasoning()`` step before
|
||||
task execution and lets the executor drive planning itself. This is a
|
||||
class-level capability flag so callers can query it without instantiating
|
||||
the executor (e.g. ``self.executor_class.supports_internal_planning``).
|
||||
"""
|
||||
|
||||
supports_kickoff: ClassVar[bool] = False
|
||||
"""Whether this executor supports the standalone ``Agent.kickoff()`` flow.
|
||||
|
||||
Executors that do not implement the Flow-based kickoff loop should leave
|
||||
this ``False``; ``Agent.kickoff()`` will then refuse to silently fall back
|
||||
to a different executor and instead emit a ``DeprecationWarning`` so the
|
||||
silent override is visible to the caller.
|
||||
"""
|
||||
|
||||
crew: Crew | None = Field(default=None, exclude=True)
|
||||
agent: BaseAgent | None = Field(default=None, exclude=True)
|
||||
task: Task | None = Field(default=None, exclude=True)
|
||||
|
||||
@@ -156,6 +156,36 @@ def _resolve_agents(value: Any, info: Any) -> Any:
|
||||
return [_resolve_agent(a, info) for a in value]
|
||||
|
||||
|
||||
def _mcp_label(mcp: Any) -> str:
|
||||
if isinstance(mcp, str):
|
||||
return mcp
|
||||
url = getattr(mcp, "url", None)
|
||||
if url:
|
||||
return str(url)
|
||||
command = getattr(mcp, "command", None)
|
||||
if command:
|
||||
return str(command)
|
||||
return type(mcp).__name__
|
||||
|
||||
|
||||
def _app_placeholder(app: Any) -> str:
|
||||
raw = app if isinstance(app, str) else str(app)
|
||||
if "#" in raw:
|
||||
base, action = raw.split("#", 1)
|
||||
return f"app:{base}:{action}"
|
||||
return f"app:{raw}:*"
|
||||
|
||||
|
||||
def _dedupe_preserve_order(values: list[str]) -> list[str]:
|
||||
seen: set[str] = set()
|
||||
out: list[str] = []
|
||||
for v in values:
|
||||
if v not in seen:
|
||||
seen.add(v)
|
||||
out.append(v)
|
||||
return out
|
||||
|
||||
|
||||
class Crew(FlowTrackable, BaseModel):
|
||||
"""
|
||||
Represents a group of agents, defining how they should collaborate and the
|
||||
@@ -1927,6 +1957,81 @@ class Crew(FlowTrackable, BaseModel):
|
||||
|
||||
return required_inputs
|
||||
|
||||
def list_tools(self) -> dict[str, list[str]]:
|
||||
"""Enumerate tool names available to each agent in this Crew.
|
||||
|
||||
Mirrors the runtime tool resolution in ``_prepare_tools`` without
|
||||
performing any I/O: tools sourced from external services (MCP
|
||||
servers, platform apps) are returned as ``"mcp:<id>:*"`` /
|
||||
``"app:<id>[:action]"`` placeholders since their concrete tool
|
||||
names require live fetches.
|
||||
|
||||
Returns:
|
||||
Mapping of agent role to a deduplicated list of tool names.
|
||||
In hierarchical mode the manager agent is included as an
|
||||
extra entry keyed by its role.
|
||||
"""
|
||||
tasks_by_agent: dict[int, list[Task]] = {}
|
||||
for task in self.tasks:
|
||||
if task.agent is not None:
|
||||
tasks_by_agent.setdefault(id(task.agent), []).append(task)
|
||||
|
||||
result: dict[str, list[str]] = {}
|
||||
|
||||
for agent in self.agents:
|
||||
candidates: list[str] = [tool.name for tool in agent.tools or []]
|
||||
|
||||
for task in tasks_by_agent.get(id(agent), []):
|
||||
candidates.extend(tool.name for tool in task.tools or [])
|
||||
|
||||
if (
|
||||
self.process != Process.hierarchical
|
||||
and getattr(agent, "allow_delegation", False)
|
||||
and len(self.agents) > 1
|
||||
):
|
||||
candidates.append("Delegate work to coworker")
|
||||
candidates.append("Ask question to coworker")
|
||||
|
||||
if getattr(agent, "multimodal", False):
|
||||
llm = getattr(agent, "llm", None)
|
||||
if not (isinstance(llm, BaseLLM) and llm.supports_multimodal()):
|
||||
candidates.append("Add image to content")
|
||||
|
||||
if getattr(agent, "memory", None) is not None or self._memory is not None:
|
||||
candidates.append("Search memory")
|
||||
candidates.append("Save to memory")
|
||||
|
||||
candidates.extend(
|
||||
f"mcp:{_mcp_label(mcp)}:*" for mcp in getattr(agent, "mcps", None) or []
|
||||
)
|
||||
candidates.extend(
|
||||
_app_placeholder(app) for app in getattr(agent, "apps", None) or []
|
||||
)
|
||||
|
||||
for task in tasks_by_agent.get(id(agent), []):
|
||||
if get_all_files(self.id, task.id):
|
||||
candidates.append("read_file")
|
||||
break
|
||||
|
||||
result[agent.role] = _dedupe_preserve_order(candidates)
|
||||
|
||||
if self.process == Process.hierarchical:
|
||||
if self.manager_agent is not None:
|
||||
mgr_candidates: list[str] = [
|
||||
tool.name for tool in self.manager_agent.tools or []
|
||||
]
|
||||
mgr_role = self.manager_agent.role
|
||||
else:
|
||||
mgr_candidates = []
|
||||
mgr_role = get_i18n(prompt_file=self.prompt_file).retrieve(
|
||||
"hierarchical_manager_agent", "role"
|
||||
)
|
||||
mgr_candidates.append("Delegate work to coworker")
|
||||
mgr_candidates.append("Ask question to coworker")
|
||||
result[mgr_role] = _dedupe_preserve_order(mgr_candidates)
|
||||
|
||||
return result
|
||||
|
||||
def copy(self) -> Crew: # type: ignore[override]
|
||||
"""
|
||||
Creates a deep copy of the Crew instance.
|
||||
|
||||
@@ -9,7 +9,7 @@ from datetime import datetime
|
||||
import inspect
|
||||
import json
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar, cast
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
|
||||
from uuid import uuid4
|
||||
|
||||
from crewai_core.printer import PRINTER
|
||||
@@ -172,10 +172,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor):
|
||||
_skip_auto_memory: bool = True
|
||||
|
||||
executor_type: Literal["experimental"] = "experimental"
|
||||
|
||||
supports_internal_planning: ClassVar[bool] = True
|
||||
supports_kickoff: ClassVar[bool] = True
|
||||
|
||||
suppress_flow_events: bool = True # always suppress for executor
|
||||
llm: BaseLLM = Field(exclude=True)
|
||||
prompt: SystemPromptResult | StandardPromptResult = Field(exclude=True)
|
||||
|
||||
@@ -1864,6 +1864,34 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
except (AttributeError, TypeError):
|
||||
return "" # Safely handle any unexpected attribute access issues
|
||||
|
||||
def list_tools(self) -> dict[str, dict[str, list[str]]]:
|
||||
"""Enumerate tools available across the Crews attached to this Flow.
|
||||
|
||||
Inspects public instance attributes for :class:`~crewai.crew.Crew`
|
||||
instances and lists/tuples of Crews, delegating to
|
||||
:meth:`Crew.list_tools` for each. Crews instantiated lazily inside
|
||||
flow methods are not discovered — to opt them in, store them as
|
||||
instance attributes (typically in ``__init__``).
|
||||
|
||||
Returns:
|
||||
Mapping of crew identifier (the attribute name, with
|
||||
``[index]`` suffix when stored in a list/tuple) to that
|
||||
Crew's ``list_tools()`` output.
|
||||
"""
|
||||
from crewai.crew import Crew
|
||||
|
||||
result: dict[str, dict[str, list[str]]] = {}
|
||||
for attr_name, value in vars(self).items():
|
||||
if attr_name.startswith("_"):
|
||||
continue
|
||||
if isinstance(value, Crew):
|
||||
result[attr_name] = value.list_tools()
|
||||
elif isinstance(value, (list, tuple)):
|
||||
for i, item in enumerate(value):
|
||||
if isinstance(item, Crew):
|
||||
result[f"{attr_name}[{i}]"] = item.list_tools()
|
||||
return result
|
||||
|
||||
def _initialize_state(self, inputs: dict[str, Any]) -> None:
|
||||
"""Initialize or update flow state with new inputs.
|
||||
|
||||
|
||||
@@ -1,349 +0,0 @@
|
||||
"""Regression tests for issue #5736: parallel agent executors must be consistent.
|
||||
|
||||
The repository has historically carried two parallel ``BaseAgentExecutor``
|
||||
subclasses — :class:`crewai.agents.crew_agent_executor.CrewAgentExecutor` and
|
||||
:class:`crewai.experimental.agent_executor.AgentExecutor`. The issue reported
|
||||
two concrete bugs caused by the split:
|
||||
|
||||
1. ``Agent.kickoff()`` silently ignored ``Agent.executor_class`` and always
|
||||
instantiated ``AgentExecutor``, so the user-visible default
|
||||
(``CrewAgentExecutor``) did not match the executor that ``kickoff()``
|
||||
actually used.
|
||||
2. The ``handle_reasoning()`` step was gated on a literal class identity check
|
||||
(``self.executor_class is not AgentExecutor``), so reasoning was silently
|
||||
skipped depending on the executor and could not be controlled via a
|
||||
capability flag.
|
||||
|
||||
These tests pin down the new contract:
|
||||
|
||||
* Capability flags (``supports_internal_planning`` / ``supports_kickoff``) are
|
||||
the single source of truth for executor differences.
|
||||
* ``Agent.kickoff()`` honors ``executor_class`` when it is kickoff-capable and
|
||||
otherwise emits a ``DeprecationWarning`` instead of silently overriding.
|
||||
* ``Agent.execute_task()`` instantiates ``self.executor_class`` (not a
|
||||
hard-coded class).
|
||||
* ``handle_reasoning()`` is gated on the capability flag, not class identity.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, ClassVar, Literal
|
||||
import warnings
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai import Agent, Task
|
||||
from crewai.agents.agent_builder.base_agent_executor import BaseAgentExecutor
|
||||
from crewai.agents.crew_agent_executor import CrewAgentExecutor
|
||||
from crewai.experimental.agent_executor import AgentExecutor
|
||||
from crewai.llms.base_llm import BaseLLM
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_llm() -> MagicMock:
|
||||
"""Create a mock LLM that satisfies BaseLLM's protocol."""
|
||||
llm = MagicMock(spec=BaseLLM)
|
||||
llm.supports_stop_words.return_value = True
|
||||
llm.stop = []
|
||||
llm.model = "gpt-4o-mini"
|
||||
return llm
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_agent(mock_llm: MagicMock) -> Agent:
|
||||
"""A plain Agent that uses default ``executor_class``."""
|
||||
return Agent(
|
||||
role="Test Agent",
|
||||
goal="Verify executor consistency",
|
||||
backstory="An agent used by issue #5736 regression tests.",
|
||||
llm=mock_llm,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Capability flags
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestExecutorCapabilityFlags:
|
||||
"""``supports_internal_planning`` / ``supports_kickoff`` advertise the
|
||||
differences between executors so callers don't have to ``isinstance``-check
|
||||
against a specific class."""
|
||||
|
||||
def test_base_executor_has_no_capabilities(self) -> None:
|
||||
assert BaseAgentExecutor.supports_internal_planning is False
|
||||
assert BaseAgentExecutor.supports_kickoff is False
|
||||
|
||||
def test_crew_agent_executor_has_no_capabilities(self) -> None:
|
||||
"""The legacy executor neither plans internally nor handles kickoff."""
|
||||
assert CrewAgentExecutor.supports_internal_planning is False
|
||||
assert CrewAgentExecutor.supports_kickoff is False
|
||||
|
||||
def test_agent_executor_advertises_full_capabilities(self) -> None:
|
||||
"""The Flow-based executor implements both plan-and-execute and kickoff."""
|
||||
assert AgentExecutor.supports_internal_planning is True
|
||||
assert AgentExecutor.supports_kickoff is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent.kickoff() honors executor_class
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestKickoffRespectsExecutorClass:
|
||||
"""Issue #5736 part 1: ``Agent.kickoff()`` must honor ``executor_class``."""
|
||||
|
||||
def test_kickoff_uses_explicit_agent_executor_without_warning(
|
||||
self, mock_llm: MagicMock
|
||||
) -> None:
|
||||
"""When the user explicitly opts into ``AgentExecutor``, no warning fires."""
|
||||
agent = Agent(
|
||||
role="r",
|
||||
goal="g",
|
||||
backstory="b",
|
||||
llm=mock_llm,
|
||||
executor_class=AgentExecutor,
|
||||
)
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
executor_cls = agent._resolve_kickoff_executor_class()
|
||||
|
||||
assert executor_cls is AgentExecutor
|
||||
kickoff_warnings = [
|
||||
w for w in caught if "Agent.kickoff()" in str(w.message)
|
||||
]
|
||||
assert kickoff_warnings == [], (
|
||||
f"Did not expect any kickoff warning, got: {[str(w.message) for w in kickoff_warnings]}"
|
||||
)
|
||||
|
||||
def test_kickoff_uses_kickoff_capable_subclass_of_agent_executor(
|
||||
self, mock_llm: MagicMock
|
||||
) -> None:
|
||||
"""A user-defined subclass that opts into kickoff is honored verbatim
|
||||
(rather than being silently replaced with ``AgentExecutor``)."""
|
||||
|
||||
class CustomAgentExecutor(AgentExecutor):
|
||||
executor_type: Literal["custom"] = "custom" # type: ignore[assignment]
|
||||
supports_kickoff: ClassVar[bool] = True
|
||||
supports_internal_planning: ClassVar[bool] = True
|
||||
|
||||
agent = Agent(
|
||||
role="r",
|
||||
goal="g",
|
||||
backstory="b",
|
||||
llm=mock_llm,
|
||||
executor_class=CustomAgentExecutor,
|
||||
)
|
||||
|
||||
with warnings.catch_warnings(record=True) as caught:
|
||||
warnings.simplefilter("always")
|
||||
executor_cls = agent._resolve_kickoff_executor_class()
|
||||
|
||||
assert executor_cls is CustomAgentExecutor
|
||||
kickoff_warnings = [
|
||||
w for w in caught if "Agent.kickoff()" in str(w.message)
|
||||
]
|
||||
assert kickoff_warnings == []
|
||||
|
||||
def test_kickoff_warns_and_falls_back_when_executor_class_is_not_kickoff_capable(
|
||||
self, mock_llm: MagicMock
|
||||
) -> None:
|
||||
"""Default-configured agents (``executor_class=CrewAgentExecutor``) used
|
||||
to silently flip to ``AgentExecutor`` on ``kickoff()``. The fix keeps
|
||||
the fallback for backwards compatibility but emits a
|
||||
``DeprecationWarning`` so the silent override is now visible."""
|
||||
agent = Agent(
|
||||
role="r",
|
||||
goal="g",
|
||||
backstory="b",
|
||||
llm=mock_llm,
|
||||
executor_class=CrewAgentExecutor, # the default
|
||||
)
|
||||
|
||||
with pytest.warns(DeprecationWarning, match=r"Agent\.kickoff\(\)"):
|
||||
executor_cls = agent._resolve_kickoff_executor_class()
|
||||
|
||||
assert executor_cls is AgentExecutor
|
||||
|
||||
def test_kickoff_fallback_warning_mentions_explicit_opt_in(
|
||||
self, mock_llm: MagicMock
|
||||
) -> None:
|
||||
"""The deprecation message should tell users how to silence it — by
|
||||
setting ``executor_class=AgentExecutor`` explicitly."""
|
||||
agent = Agent(
|
||||
role="r",
|
||||
goal="g",
|
||||
backstory="b",
|
||||
llm=mock_llm,
|
||||
executor_class=CrewAgentExecutor,
|
||||
)
|
||||
|
||||
with pytest.warns(DeprecationWarning) as record:
|
||||
agent._resolve_kickoff_executor_class()
|
||||
|
||||
assert any(
|
||||
"executor_class=AgentExecutor" in str(w.message) for w in record
|
||||
)
|
||||
|
||||
def test_kickoff_constructs_executor_via_resolve_helper(
|
||||
self, test_agent: Agent
|
||||
) -> None:
|
||||
"""``_prepare_kickoff()`` must route through ``_resolve_kickoff_executor_class``
|
||||
rather than instantiating ``AgentExecutor`` directly. This guards against
|
||||
regressing back to the issue #5736 hardcoded-class behavior."""
|
||||
sentinel = MagicMock(name="sentinel_executor_cls")
|
||||
sentinel.return_value = MagicMock(name="sentinel_executor")
|
||||
|
||||
with patch.object(
|
||||
test_agent,
|
||||
"_resolve_kickoff_executor_class",
|
||||
return_value=sentinel,
|
||||
) as mock_resolve, patch.object(
|
||||
test_agent, "_build_execution_prompt"
|
||||
) as mock_build_prompt:
|
||||
mock_build_prompt.return_value = (
|
||||
{"system": "sys"},
|
||||
["Observation:"],
|
||||
None,
|
||||
)
|
||||
test_agent._prepare_kickoff(messages="hello")
|
||||
|
||||
mock_resolve.assert_called_once_with()
|
||||
sentinel.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent.execute_task() honors executor_class (sanity check; pre-existing
|
||||
# behavior we don't want to regress)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestExecuteTaskRespectsExecutorClass:
|
||||
"""``Agent.execute_task()`` already routed through ``self.executor_class``
|
||||
— these tests pin that contract so future refactors keep both entry points
|
||||
honoring the user's executor choice."""
|
||||
|
||||
def test_execute_task_instantiates_self_executor_class(
|
||||
self, test_agent: Agent
|
||||
) -> None:
|
||||
"""The non-resuming branch of ``Agent.create_agent_executor`` should
|
||||
call ``self.executor_class(...)``. Stubbing ``executor_class`` with a
|
||||
factory lets us observe the instantiation without running the LLM
|
||||
loop."""
|
||||
captured: dict[str, Any] = {}
|
||||
|
||||
def fake_factory(*args: Any, **kwargs: Any) -> MagicMock:
|
||||
captured["called"] = True
|
||||
captured["kwargs"] = kwargs
|
||||
return MagicMock(spec=CrewAgentExecutor)
|
||||
|
||||
task = Task(
|
||||
description="d",
|
||||
expected_output="e",
|
||||
agent=test_agent,
|
||||
)
|
||||
test_agent.agent_executor = None
|
||||
test_agent.executor_class = fake_factory # type: ignore[assignment]
|
||||
|
||||
with patch.object(test_agent, "_build_execution_prompt") as mock_bp:
|
||||
mock_bp.return_value = ({"system": "s"}, ["Obs:"], None)
|
||||
test_agent.create_agent_executor(task=task)
|
||||
|
||||
assert captured.get("called") is True
|
||||
# The agent passed itself to its executor.
|
||||
assert captured["kwargs"].get("agent") is test_agent
|
||||
assert captured["kwargs"].get("task") is task
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# handle_reasoning gate: capability flag, not class identity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestReasoningGateUsesCapabilityFlag:
|
||||
"""Issue #5736 part 2: the reasoning gate must consult
|
||||
``supports_internal_planning`` rather than testing class identity. This
|
||||
way a ``CrewAgentExecutor`` subclass that opts into internal planning is
|
||||
treated correctly — and so is an ``AgentExecutor`` subclass that doesn't."""
|
||||
|
||||
def test_reasoning_runs_for_executor_without_internal_planning(
|
||||
self, test_agent: Agent
|
||||
) -> None:
|
||||
"""Default ``executor_class`` is ``CrewAgentExecutor`` which does NOT
|
||||
plan internally, so the legacy ``handle_reasoning`` step must run."""
|
||||
task = Task(description="d", expected_output="e", agent=test_agent)
|
||||
|
||||
assert test_agent.executor_class is CrewAgentExecutor
|
||||
assert (
|
||||
getattr(
|
||||
test_agent.executor_class, "supports_internal_planning", False
|
||||
)
|
||||
is False
|
||||
)
|
||||
|
||||
with patch(
|
||||
"crewai.agent.core.handle_reasoning"
|
||||
) as mock_handle_reasoning, patch.object(
|
||||
test_agent, "_inject_date_to_task"
|
||||
), patch.object(
|
||||
test_agent, "_retrieve_memory_context", return_value="prompt"
|
||||
):
|
||||
test_agent._prepare_task_execution(task=task, context=None)
|
||||
|
||||
mock_handle_reasoning.assert_called_once_with(test_agent, task)
|
||||
|
||||
def test_reasoning_skipped_for_executor_with_internal_planning(
|
||||
self, test_agent: Agent
|
||||
) -> None:
|
||||
"""``AgentExecutor.supports_internal_planning is True``, so the agent
|
||||
should not invoke the external ``handle_reasoning`` step (the executor
|
||||
runs its own ``generate_plan`` flow instead)."""
|
||||
test_agent.executor_class = AgentExecutor # type: ignore[assignment]
|
||||
task = Task(description="d", expected_output="e", agent=test_agent)
|
||||
|
||||
assert (
|
||||
getattr(
|
||||
test_agent.executor_class, "supports_internal_planning", False
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
with patch(
|
||||
"crewai.agent.core.handle_reasoning"
|
||||
) as mock_handle_reasoning, patch.object(
|
||||
test_agent, "_inject_date_to_task"
|
||||
), patch.object(
|
||||
test_agent, "_retrieve_memory_context", return_value="prompt"
|
||||
):
|
||||
test_agent._prepare_task_execution(task=task, context=None)
|
||||
|
||||
mock_handle_reasoning.assert_not_called()
|
||||
|
||||
def test_reasoning_gate_does_not_use_isinstance_or_class_identity(
|
||||
self, test_agent: Agent
|
||||
) -> None:
|
||||
"""A ``CrewAgentExecutor`` subclass that opts into internal planning
|
||||
(``supports_internal_planning = True``) must skip the legacy reasoning
|
||||
step. This is the key behavioral promise of replacing the old
|
||||
``self.executor_class is not AgentExecutor`` check with a flag-based
|
||||
check."""
|
||||
|
||||
class PlanningCrewExecutor(CrewAgentExecutor):
|
||||
executor_type: Literal["crew_planning"] = "crew_planning" # type: ignore[assignment]
|
||||
supports_internal_planning: ClassVar[bool] = True
|
||||
|
||||
test_agent.executor_class = PlanningCrewExecutor # type: ignore[assignment]
|
||||
task = Task(description="d", expected_output="e", agent=test_agent)
|
||||
|
||||
with patch(
|
||||
"crewai.agent.core.handle_reasoning"
|
||||
) as mock_handle_reasoning, patch.object(
|
||||
test_agent, "_inject_date_to_task"
|
||||
), patch.object(
|
||||
test_agent, "_retrieve_memory_context", return_value="prompt"
|
||||
):
|
||||
test_agent._prepare_task_execution(task=task, context=None)
|
||||
|
||||
mock_handle_reasoning.assert_not_called()
|
||||
207
lib/crewai/tests/test_list_tools_crew.py
Normal file
207
lib/crewai/tests/test_list_tools_crew.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""Tests for Crew.list_tools()."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.crew import Crew
|
||||
from crewai.process import Process
|
||||
from crewai.task import Task
|
||||
from crewai.tools import BaseTool
|
||||
|
||||
|
||||
class _Args(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
def _tool(tool_name: str) -> BaseTool:
|
||||
class _T(BaseTool):
|
||||
name: str = tool_name
|
||||
description: str = "test tool"
|
||||
args_schema: type = _Args
|
||||
|
||||
def _run(self, **_: Any) -> str:
|
||||
return ""
|
||||
|
||||
return _T()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def writer():
|
||||
return Agent(role="writer", goal="g", backstory="b", tools=[_tool("search")])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def editor():
|
||||
return Agent(role="editor", goal="g", backstory="b")
|
||||
|
||||
|
||||
def test_lists_user_defined_agent_tools(writer):
|
||||
task = Task(description="d", expected_output="e", agent=writer)
|
||||
crew = Crew(agents=[writer], tasks=[task])
|
||||
|
||||
assert crew.list_tools() == {"writer": ["search"]}
|
||||
|
||||
|
||||
def test_includes_task_level_tool_overrides(writer):
|
||||
extra = _tool("calculator")
|
||||
task = Task(description="d", expected_output="e", agent=writer, tools=[extra])
|
||||
crew = Crew(agents=[writer], tasks=[task])
|
||||
|
||||
assert crew.list_tools() == {"writer": ["search", "calculator"]}
|
||||
|
||||
|
||||
def test_dedupes_when_agent_and_task_share_a_tool(writer):
|
||||
duplicate = _tool("search")
|
||||
task = Task(description="d", expected_output="e", agent=writer, tools=[duplicate])
|
||||
crew = Crew(agents=[writer], tasks=[task])
|
||||
|
||||
assert crew.list_tools() == {"writer": ["search"]}
|
||||
|
||||
|
||||
def test_peer_delegation_adds_delegate_and_ask_tools(writer, editor):
|
||||
writer.allow_delegation = True
|
||||
task = Task(description="d", expected_output="e", agent=writer)
|
||||
crew = Crew(agents=[writer, editor], tasks=[task])
|
||||
|
||||
tools = crew.list_tools()
|
||||
assert "Delegate work to coworker" in tools["writer"]
|
||||
assert "Ask question to coworker" in tools["writer"]
|
||||
assert "Delegate work to coworker" not in tools["editor"]
|
||||
|
||||
|
||||
def test_peer_delegation_skipped_when_only_one_agent(writer):
|
||||
writer.allow_delegation = True
|
||||
task = Task(description="d", expected_output="e", agent=writer)
|
||||
crew = Crew(agents=[writer], tasks=[task])
|
||||
|
||||
assert "Delegate work to coworker" not in crew.list_tools()["writer"]
|
||||
|
||||
|
||||
def test_hierarchical_includes_default_manager(writer, editor):
|
||||
writer.allow_delegation = True
|
||||
task = Task(description="d", expected_output="e", agent=writer)
|
||||
crew = Crew(
|
||||
agents=[writer, editor],
|
||||
tasks=[task],
|
||||
process=Process.hierarchical,
|
||||
manager_llm="gpt-4o-mini",
|
||||
)
|
||||
|
||||
tools = crew.list_tools()
|
||||
assert "writer" in tools
|
||||
assert "Delegate work to coworker" not in tools["writer"]
|
||||
# Default manager role from i18n.
|
||||
manager_keys = [k for k in tools if k not in {"writer", "editor"}]
|
||||
assert len(manager_keys) == 1
|
||||
manager_role = manager_keys[0]
|
||||
assert tools[manager_role] == [
|
||||
"Delegate work to coworker",
|
||||
"Ask question to coworker",
|
||||
]
|
||||
|
||||
|
||||
def test_hierarchical_uses_user_provided_manager_role(writer, editor):
|
||||
manager = Agent(role="Chief", goal="g", backstory="b", allow_delegation=True)
|
||||
task = Task(description="d", expected_output="e", agent=writer)
|
||||
crew = Crew(
|
||||
agents=[writer, editor],
|
||||
tasks=[task],
|
||||
process=Process.hierarchical,
|
||||
manager_agent=manager,
|
||||
manager_llm="gpt-4o-mini",
|
||||
)
|
||||
|
||||
tools = crew.list_tools()
|
||||
assert "Chief" in tools
|
||||
assert tools["Chief"] == [
|
||||
"Delegate work to coworker",
|
||||
"Ask question to coworker",
|
||||
]
|
||||
|
||||
|
||||
def test_multimodal_added_when_llm_does_not_support_it(writer):
|
||||
writer.multimodal = True
|
||||
task = Task(description="d", expected_output="e", agent=writer)
|
||||
crew = Crew(agents=[writer], tasks=[task])
|
||||
|
||||
with patch.object(type(writer.llm), "supports_multimodal", return_value=False):
|
||||
tools = crew.list_tools()
|
||||
|
||||
assert "Add image to content" in tools["writer"]
|
||||
|
||||
|
||||
def test_multimodal_skipped_when_llm_supports_it(writer):
|
||||
writer.multimodal = True
|
||||
task = Task(description="d", expected_output="e", agent=writer)
|
||||
crew = Crew(agents=[writer], tasks=[task])
|
||||
|
||||
with patch.object(type(writer.llm), "supports_multimodal", return_value=True):
|
||||
tools = crew.list_tools()
|
||||
|
||||
assert "Add image to content" not in tools["writer"]
|
||||
|
||||
|
||||
def test_crew_level_memory_adds_search_and_save(writer):
|
||||
task = Task(description="d", expected_output="e", agent=writer)
|
||||
crew = Crew(agents=[writer], tasks=[task], memory=True)
|
||||
|
||||
tools = crew.list_tools()
|
||||
assert "Search memory" in tools["writer"]
|
||||
assert "Save to memory" in tools["writer"]
|
||||
|
||||
|
||||
def test_no_memory_means_no_memory_tools(writer):
|
||||
task = Task(description="d", expected_output="e", agent=writer)
|
||||
crew = Crew(agents=[writer], tasks=[task]) # memory defaults to False
|
||||
|
||||
tools = crew.list_tools()
|
||||
assert "Search memory" not in tools["writer"]
|
||||
assert "Save to memory" not in tools["writer"]
|
||||
|
||||
|
||||
def test_mcp_emits_placeholder_per_server():
|
||||
a = Agent(role="r", goal="g", backstory="b", mcps=["github", "slack"])
|
||||
task = Task(description="d", expected_output="e", agent=a)
|
||||
crew = Crew(agents=[a], tasks=[task])
|
||||
|
||||
assert crew.list_tools()["r"] == ["mcp:github:*", "mcp:slack:*"]
|
||||
|
||||
|
||||
def test_apps_emit_placeholder_with_action_split():
|
||||
a = Agent(
|
||||
role="r",
|
||||
goal="g",
|
||||
backstory="b",
|
||||
apps=["gmail", "slack#send_message"],
|
||||
)
|
||||
task = Task(description="d", expected_output="e", agent=a)
|
||||
crew = Crew(agents=[a], tasks=[task])
|
||||
|
||||
assert crew.list_tools()["r"] == ["app:gmail:*", "app:slack:send_message"]
|
||||
|
||||
|
||||
def test_file_reader_added_when_task_has_input_files(writer):
|
||||
task = Task(description="d", expected_output="e", agent=writer)
|
||||
crew = Crew(agents=[writer], tasks=[task])
|
||||
|
||||
sentinel_files = {"foo.txt": object()}
|
||||
with patch("crewai.crew.get_all_files", return_value=sentinel_files):
|
||||
tools = crew.list_tools()
|
||||
|
||||
assert "read_file" in tools["writer"]
|
||||
|
||||
|
||||
def test_file_reader_not_added_when_no_input_files(writer):
|
||||
task = Task(description="d", expected_output="e", agent=writer)
|
||||
crew = Crew(agents=[writer], tasks=[task])
|
||||
|
||||
with patch("crewai.crew.get_all_files", return_value={}):
|
||||
tools = crew.list_tools()
|
||||
|
||||
assert "read_file" not in tools["writer"]
|
||||
132
lib/crewai/tests/test_list_tools_flow.py
Normal file
132
lib/crewai/tests/test_list_tools_flow.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Tests for Flow.list_tools()."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.crew import Crew
|
||||
from crewai.flow.flow import Flow, start
|
||||
from crewai.task import Task
|
||||
from crewai.tools import BaseTool
|
||||
|
||||
|
||||
class _Args(BaseModel):
|
||||
pass
|
||||
|
||||
|
||||
def _tool(tool_name: str) -> BaseTool:
|
||||
class _T(BaseTool):
|
||||
name: str = tool_name
|
||||
description: str = "test"
|
||||
args_schema: type = _Args
|
||||
|
||||
def _run(self, **_: Any) -> str:
|
||||
return ""
|
||||
|
||||
return _T()
|
||||
|
||||
|
||||
def _crew(role: str = "writer", tool_name: str = "search") -> Crew:
|
||||
agent = Agent(role=role, goal="g", backstory="b", tools=[_tool(tool_name)])
|
||||
task = Task(description="d", expected_output="e", agent=agent)
|
||||
return Crew(agents=[agent], tasks=[task])
|
||||
|
||||
|
||||
def test_empty_flow_returns_empty_dict():
|
||||
class EmptyFlow(Flow):
|
||||
@start()
|
||||
def kickoff(self):
|
||||
return None
|
||||
|
||||
assert EmptyFlow().list_tools() == {}
|
||||
|
||||
|
||||
def test_crew_attribute_keyed_by_attribute_name():
|
||||
class SingleCrewFlow(Flow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.poem_crew = _crew()
|
||||
|
||||
@start()
|
||||
def kickoff(self):
|
||||
return self.poem_crew.kickoff()
|
||||
|
||||
assert SingleCrewFlow().list_tools() == {"poem_crew": {"writer": ["search"]}}
|
||||
|
||||
|
||||
def test_list_of_crews_keyed_with_index_suffix():
|
||||
class ListFlow(Flow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.research_crews = [_crew("a", "t1"), _crew("b", "t2")]
|
||||
|
||||
@start()
|
||||
def kickoff(self):
|
||||
return None
|
||||
|
||||
tools = ListFlow().list_tools()
|
||||
assert tools == {
|
||||
"research_crews[0]": {"a": ["t1"]},
|
||||
"research_crews[1]": {"b": ["t2"]},
|
||||
}
|
||||
|
||||
|
||||
def test_tuple_of_crews_supported():
|
||||
class TupleFlow(Flow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.crews_tuple = (_crew("a", "t1"),)
|
||||
|
||||
@start()
|
||||
def kickoff(self):
|
||||
return None
|
||||
|
||||
assert TupleFlow().list_tools() == {"crews_tuple[0]": {"a": ["t1"]}}
|
||||
|
||||
|
||||
def test_underscore_prefixed_attributes_ignored():
|
||||
class HiddenFlow(Flow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._private_crew = _crew("a", "t1")
|
||||
self.public_crew = _crew("b", "t2")
|
||||
|
||||
@start()
|
||||
def kickoff(self):
|
||||
return None
|
||||
|
||||
tools = HiddenFlow().list_tools()
|
||||
assert "_private_crew" not in tools
|
||||
assert tools == {"public_crew": {"b": ["t2"]}}
|
||||
|
||||
|
||||
def test_non_crew_attributes_skipped():
|
||||
class MixedFlow(Flow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.label = "some-string"
|
||||
self.config = {"k": "v"}
|
||||
self.poem_crew = _crew()
|
||||
|
||||
@start()
|
||||
def kickoff(self):
|
||||
return None
|
||||
|
||||
assert MixedFlow().list_tools() == {"poem_crew": {"writer": ["search"]}}
|
||||
|
||||
|
||||
def test_list_with_non_crew_items_filtered():
|
||||
class PartialFlow(Flow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.things = [_crew("a", "t1"), "not a crew", 42]
|
||||
|
||||
@start()
|
||||
def kickoff(self):
|
||||
return None
|
||||
|
||||
assert PartialFlow().list_tools() == {"things[0]": {"a": ["t1"]}}
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.14.5a3"
|
||||
__version__ = "1.14.5a2"
|
||||
|
||||
6
uv.lock
generated
6
uv.lock
generated
@@ -1304,6 +1304,7 @@ dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "regex" },
|
||||
{ name = "textual" },
|
||||
{ name = "tokenizers" },
|
||||
{ name = "tomli" },
|
||||
{ name = "tomli-w" },
|
||||
@@ -1420,6 +1421,7 @@ requires-dist = [
|
||||
{ name = "qdrant-client", extras = ["fastembed"], marker = "extra == 'qdrant'", specifier = "~=1.14.3" },
|
||||
{ name = "qdrant-edge-py", marker = "extra == 'qdrant-edge'", specifier = ">=0.6.0" },
|
||||
{ name = "regex", specifier = "~=2026.1.15" },
|
||||
{ name = "textual", specifier = ">=7.5.0" },
|
||||
{ name = "tiktoken", marker = "extra == 'embeddings'", specifier = ">=0.8.0,<0.13" },
|
||||
{ name = "tokenizers", specifier = ">=0.21,<1" },
|
||||
{ name = "tomli", specifier = "~=2.0.2" },
|
||||
@@ -1433,7 +1435,6 @@ name = "crewai-cli"
|
||||
source = { editable = "lib/cli" }
|
||||
dependencies = [
|
||||
{ name = "appdirs" },
|
||||
{ name = "certifi" },
|
||||
{ name = "click" },
|
||||
{ name = "crewai-core" },
|
||||
{ name = "cryptography" },
|
||||
@@ -1444,7 +1445,6 @@ dependencies = [
|
||||
{ name = "pyjwt" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "rich" },
|
||||
{ name = "textual" },
|
||||
{ name = "tomli" },
|
||||
{ name = "tomli-w" },
|
||||
{ name = "uv" },
|
||||
@@ -1453,7 +1453,6 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "appdirs", specifier = "~=1.4.4" },
|
||||
{ name = "certifi" },
|
||||
{ name = "click", specifier = "~=8.1.7" },
|
||||
{ name = "crewai-core", editable = "lib/crewai-core" },
|
||||
{ name = "cryptography", specifier = ">=42.0" },
|
||||
@@ -1464,7 +1463,6 @@ requires-dist = [
|
||||
{ name = "pyjwt", specifier = ">=2.9.0,<3" },
|
||||
{ name = "python-dotenv", specifier = ">=1.2.2,<2" },
|
||||
{ name = "rich", specifier = ">=13.7.1" },
|
||||
{ name = "textual", specifier = ">=7.5.0" },
|
||||
{ name = "tomli", specifier = "~=2.0.2" },
|
||||
{ name = "tomli-w", specifier = "~=1.1.0" },
|
||||
{ name = "uv", specifier = "~=0.11.6" },
|
||||
|
||||
Reference in New Issue
Block a user